flexwave / wysiwyg
A modern, feature-rich WYSIWYG editor package for Laravel 10+
Requires
- php: ^8.2
- illuminate/filesystem: ^10.0|^11.0
- illuminate/http: ^10.0|^11.0
- illuminate/routing: ^10.0|^11.0
- illuminate/support: ^10.0|^11.0
- intervention/image: ^3.0
Requires (Dev)
- laravel/pint: ^1.0
- orchestra/testbench: ^8.0|^9.0
- phpunit/phpunit: ^10.0|^11.0
README
A modern, feature-rich WYSIWYG rich-text editor package for Laravel 10+ and PHP 8.2+.
Features
- Blade component
<x-flexwave-editor />— drop in anywhere - Formatting: bold, italic, underline, strikethrough
- Headings H1–H6, paragraph
- Ordered & unordered lists, blockquotes
- Inline code & code blocks
- Link insertion (modal) + image upload
- Text alignment (left / center / right / justify)
- Drag & drop and paste images directly into the editor
- HTML source view & live preview panel
- Fullscreen editing mode
- Word count / character count status bar
- Dark mode (
auto,light,dark) - Configurable toolbar via
config/flexwave-wysiwyg.php - Laravel events on upload/delete (
ImageUploaded,ImageDeleted) - Optional image resizing via Intervention Image v3
- Custom plugin support
- Public API (
FlexWave.getInstance(id)) - Fully responsive
- Zero JS dependencies (vanilla JS, ~12 KB)
Requirements
| Requirement | Version |
|---|---|
| PHP | ^8.2 |
| Laravel | ^10.0 or ^11.0 |
| Intervention Image | ^3.0 |
Installation
1. Install via Composer
composer require flexwave/wysiwyg
The package is auto-discovered via Laravel's package auto-discovery (no need to add the provider manually).
2. Publish assets
# Publish everything (config + views + assets) php artisan vendor:publish --tag=flexwave-wysiwyg # Or publish individually: php artisan vendor:publish --tag=flexwave-wysiwyg-config php artisan vendor:publish --tag=flexwave-wysiwyg-views php artisan vendor:publish --tag=flexwave-wysiwyg-assets
3. Create the storage symlink (if using the public disk)
php artisan storage:link
4. Include assets in your layout
Add to your main Blade layout (e.g. resources/views/layouts/app.blade.php):
<head> @wysiwygStyles </head> <body> {{-- page content --}} @wysiwygScripts </body>
Or use standard HTML tags after publishing assets:
<link rel="stylesheet" href="{{ asset('vendor/flexwave-wysiwyg/css/editor.css') }}"> <script src="{{ asset('vendor/flexwave-wysiwyg/js/editor.js') }}" defer></script>
Usage
Basic usage inside a Blade form
<form method="POST" action="/posts"> @csrf <x-flexwave-editor name="content" :value="old('content', $post->content ?? '')" placeholder="Write your post content here..." /> <button type="submit">Save</button> </form>
Component attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
name |
string |
content |
HTML form field name |
value |
string |
'' |
Initial HTML content |
placeholder |
string |
(from config) | Placeholder text when empty |
height |
int |
400 |
Minimum editor height in pixels |
id |
string |
(auto-generated) | HTML id for the editor wrapper |
required |
bool |
false |
Mark textarea as required |
readonly |
bool |
false |
Disable editing |
dark-mode |
string |
auto |
auto, light, or dark |
class |
string |
'' |
Extra CSS classes on the wrapper |
Validation errors
The component automatically reads and displays Laravel validation errors:
// In your controller: $request->validate([ 'content' => 'required|string|min:10', ]);
{{-- In your view: --}} <x-flexwave-editor name="content" :value="old('content')" /> @error('content') <p>{{ $message }}</p> @enderror
(The component renders the error message automatically — no need for a separate @error block.)
Server-side Helpers
Use the Wysiwyg facade for server-side processing:
use FlexWave\Wysiwyg\Facades\Wysiwyg; // Sanitize untrusted HTML from the editor $clean = Wysiwyg::sanitize($request->input('content')); // Convert HTML to plain text $text = Wysiwyg::toText($html); // Get a plain-text excerpt $excerpt = Wysiwyg::excerpt($html, 160); // Word count $words = Wysiwyg::wordCount($html);
Configuration
config/flexwave-wysiwyg.php (after publishing):
return [ // Route prefix for upload endpoints 'route_prefix' => 'flexwave', // Middleware applied to upload routes 'middleware' => ['web', 'auth'], // File upload settings 'upload' => [ 'disk' => env('FLEXWAVE_UPLOAD_DISK', 'public'), 'path' => env('FLEXWAVE_UPLOAD_PATH', 'wysiwyg/uploads'), 'max_size' => 5120, // KB 'allowed' => ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], ], // Optional image resizing (Intervention Image) 'image_resize' => [ 'enabled' => true, 'max_width' => 1920, 'max_height' => 1080, 'quality' => 85, ], // Toolbar groups (remove items to hide them) 'toolbar' => [ ['heading', 'paragraph'], ['bold', 'italic', 'underline', 'strikethrough'], // ... ], // Editor defaults 'defaults' => [ 'height' => 400, 'placeholder' => 'Start writing here...', 'dark_mode' => 'auto', // 'auto' | 'light' | 'dark' ], ];
Environment variables
FLEXWAVE_UPLOAD_DISK=public FLEXWAVE_UPLOAD_PATH=wysiwyg/uploads FLEXWAVE_MAX_SIZE=5120 FLEXWAVE_RESIZE_ENABLED=true FLEXWAVE_RESIZE_MAX_WIDTH=1920 FLEXWAVE_RESIZE_MAX_HEIGHT=1080 FLEXWAVE_RESIZE_QUALITY=85
Events
Listen to upload events in EventServiceProvider or using #[AsEventListener]:
use FlexWave\Wysiwyg\Events\ImageUploaded; use FlexWave\Wysiwyg\Events\ImageDeleted; // EventServiceProvider.php protected $listen = [ ImageUploaded::class => [ \App\Listeners\LogImageUpload::class, ], ];
ImageUploaded properties:
| Property | Type | Description |
|---|---|---|
$path |
string |
Storage path of the uploaded file |
$url |
string |
Public URL of the uploaded file |
$disk |
string |
Laravel disk used |
$user |
?User |
Authenticated user (or null) |
JavaScript API
// Get instance by editor ID or wrapper element const editor = FlexWave.getInstance('fw-editor-abc123'); // Get the current HTML content const html = editor.getHTML(); // Set content programmatically editor.setHTML('<p>Hello <strong>world</strong>!</p>'); // Clear the editor editor.clear(); // Focus the editor editor.focus(); // Set dark mode at runtime editor.setDarkMode('dark'); // 'auto' | 'light' | 'dark'
JavaScript Events
Listen for editor events on the wrapper element:
const wrapper = document.querySelector('[data-fw-editor]'); wrapper.addEventListener('fw:init', e => console.log('Editor ready', e.detail)); wrapper.addEventListener('fw:change', e => console.log('HTML changed', e.detail.html)); wrapper.addEventListener('fw:uploadStart', e => console.log('Uploading', e.detail.file)); wrapper.addEventListener('fw:uploadSuccess', e => console.log('Uploaded', e.detail.url)); wrapper.addEventListener('fw:uploadError', e => console.warn('Upload failed', e.detail)); wrapper.addEventListener('fw:linkInserted', e => console.log('Link added', e.detail.href)); wrapper.addEventListener('fw:fullscreen', e => console.log('Fullscreen:', e.detail.active)); wrapper.addEventListener('fw:pluginLoaded', e => console.log('Plugin loaded', e.detail.src));
Custom Plugins
Create a JS file and register it in the config:
// config/flexwave-wysiwyg.php 'plugins' => [ '/js/my-wysiwyg-plugin.js', ],
In your plugin file:
// /public/js/my-wysiwyg-plugin.js document.querySelectorAll('[data-fw-editor]').forEach(wrapper => { wrapper.addEventListener('fw:init', ({ detail }) => { const editor = FlexWave.getInstance(detail.editorId); // extend the editor here }); });
Security
- All upload routes use the configured
middleware(default:['web', 'auth']), ensuring only authenticated users can upload. - File type is validated by MIME type, not just extension.
- Delete requests are restricted to the configured upload path prefix.
- The
Wysiwyg::sanitize()helper strips disallowed HTML tags and blocksjavascript:inhref/srcattributes.
License
MIT — © 2026 FlexWave