kahusoftware / filament-ckeditor-field
A basic CKEditor 5 form field configured with non-premium features.
Package info
github.com/Kahu-Software-LLC/filament-ckeditor-field
pkg:composer/kahusoftware/filament-ckeditor-field
Requires
- php: ^8.3
- ext-dom: *
- filament/forms: ^4.0
- spatie/laravel-package-tools: ^1.15.0
Requires (Dev)
- nunomaduro/collision: ^8.1
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.0|^4.0
- pestphp/pest-plugin-arch: ^3.0|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
- phpstan/phpstan: ^2.1
This package is auto-updated.
Last update: 2026-08-05 16:11:48 UTC
README
Note: This branch (
1.x) is specifically for FilamentPHP 3.x. If you're using FilamentPHP 4.x, please use the2.xbranch.
Features
- CKEditor 5 integration for FilamentPHP 3 forms
- Works anywhere Livewire renders, including panels, repeaters and standalone components
- Image upload support with configurable upload URLs
- Full control over image upload handling - you implement your own upload endpoint
- Full control over the editor configuration from config, a service provider, or per field
- Fixed or minimum editing area height, and support for Filament's
autofocus() - Helper for finding images removed from a document, so storage can be reconciled on save
- Highly customizable with fluent API
- Non-premium features only (free and open-source)
- Easy to configure and use
Table of contents
- Filament CKEditor Field
- Features
- Table of contents
- Installation
- Usage
- Configuration
- Editor options
- Available methods
- uploadUrl(
string|Closure|null$uploadUrl) - name(
string$name) - placeholder(
string$placeholder) - height(
string|Closure|null$height) - minHeight(
string|Closure|null$minHeight) - editorOptions(
array|Closure$options) - disablePlugins(
array|Closure$plugins) - enablePlugins(
array|Closure$plugins) - disableToolbarItems(
array|Closure$items)
- uploadUrl(
- Cleaning up removed images
- Inherited field methods
- Testing
- Changelog
- Contributing
- Security Vulnerabilities
- Credits
- License
Installation
You can install the field via composer:
composer require kahusoftware/filament-ckeditor-field
You can publish the config file with:
php artisan vendor:publish --tag="filament-ckeditor-field-config"
Usage
Basic usage:
use Kahusoftware\FilamentCkeditorField\CKEditor; CKEditor::make('content') ->uploadUrl(null)
Configuration
This is the contents of the published config file:
return [ /** * Image upload enabled */ 'upload_enabled' => true, /** * Image URL to upload to if one is not specified on the form field's ->uploadUrl() method */ 'upload_url' => null, /** * Everything the CKEditor instance is built from: plugins, toolbar, * htmlSupport, headings, styles and so on. See "Editor options" below. */ 'editor' => [ 'plugins' => [/* ... */], 'upload_only_plugins' => ['ImageInsert', 'ImageUpload', 'SimpleUploadAdapter'], 'upload_only_toolbar_items' => ['insertImage'], 'disabled_plugins' => [], 'disabled_toolbar_items' => [], 'options' => [/* ... */], ], ];
Editor options
The full CKEditor configuration lives under the editor key of the config file.
Publish the config to see and edit the complete defaults.
Any key the published config omits falls back to the package default, so a
partial editor block is always safe: declaring only disabled_plugins, for
example, leaves the default plugin list, toolbar and option groups intact.
Useful references while editing:
Upgrading from 1.0.x
A config file published before 1.1.0 has no editor key and keeps working
unchanged: the package defaults apply, and the editor behaves exactly as it did
before the configuration was extracted. To see and edit the full defaults,
republish the config:
php artisan vendor:publish --tag="filament-ckeditor-field-config" --force
--force overwrites the existing file, so re-apply any upload_enabled or
upload_url customizations afterwards. Alternatively, add just the editor
keys you want to change; everything omitted falls back to the defaults.
Where to set them
Options resolve in four layers, and later layers win: the package defaults,
then the published config file, then CKEditor::configureUsing(), then the
methods on an individual field.
1. Application wide, in the published config file.
// config/filament-ckeditor-field.php 'editor' => [ 'disabled_plugins' => ['FontColor', 'FontBackgroundColor', 'Highlight'], 'disabled_toolbar_items' => ['fontColor', 'fontBackgroundColor', 'highlight'], ],
2. Application wide, in a service provider.
use Kahusoftware\FilamentCkeditorField\CKEditor; public function boot(): void { CKEditor::configureUsing(fn (CKEditor $field) => $field ->disablePlugins(['FontColor', 'FontBackgroundColor', 'Highlight']) ->disableToolbarItems(['fontColor', 'fontBackgroundColor', 'highlight'])); }
3. On a single field.
CKEditor::make('content') ->editorOptions([ 'menuBar' => ['isVisible' => false], ]) ->disableToolbarItems(['insertTable'])
How options merge
String-keyed arrays merge recursively, while list-shaped arrays are replaced outright. Overriding a list therefore swaps it wholesale rather than appending to it, and passing an empty array clears it.
// Replaces the seven default sizes with two, rather than adding to them. ->editorOptions(['fontSize' => ['options' => [12, 16]]]) // Leaves link.addTargetToExternalLinks untouched. ->editorOptions(['link' => ['defaultProtocol' => 'http://']])
Plugins and toolbar items
Plugins are named as strings and resolved against the JavaScript window scope
at runtime. Names that are not bundled are skipped, so removing plugins is
always safe while adding one requires it to be present in the bundle.
Removing a toolbar item hides its button. Removing a plugin switches the
feature off entirely, which also hands whatever markup it owned back to
General HTML Support.
That distinction matters: an htmlSupport.disallow rule has no effect while the
plugin that owns the markup is still active, because the plugin's own converters
handle it first. To strip markup rather than merely hide a button, disable the
plugin and disallow the markup.
CKEditor::make('content') ->disablePlugins(['FontColor', 'FontBackgroundColor', 'Highlight']) ->disableToolbarItems(['fontColor', 'fontBackgroundColor', 'highlight']) ->editorOptions([ 'htmlSupport' => [ 'allow' => [[ 'name' => 'js:/^.*$/', 'classes' => true, 'attributes' => true, // An explicit allowlist, rather than `true` for every style. 'styles' => ['text-align', 'font-size', 'font-family'], ]], 'disallow' => [ ['name' => 'js:/^(font|mark)$/'], ['attributes' => ['bgcolor', 'color']], ['styles' => ['color', 'background', 'background-color']], ], ], ])
A toolbar item is also dropped when the plugin behind it is not in the resolved
plugin list, so disabling a plugin never leaves a dead button that CKEditor
would report as toolbarview-item-unavailable. The disableToolbarItems() call
above is therefore optional; it is kept to show both methods side by side. Items
this package does not recognise, such as buttons from a custom build, always
pass through.
Separators (|) left with nothing to divide are dropped automatically, so
removing items never leaves stray dividers in the toolbar.
JavaScript expressions
Some CKEditor options expect values that JSON cannot express, such as the
regular expression in htmlSupport. Prefix a string with js: and it is written
into the page as a bare JavaScript expression instead of a quoted string:
'name' => 'js:/^.*$/',
At runtime you can also pass a Filament\Support\RawJs instance. In the config
file use the js: string form, because objects do not survive
php artisan config:cache.
Note A
js:value is emitted verbatim. Only use it for values you control, never for user input.
Available methods
uploadUrl(string | Closure | null $uploadUrl)
Sets the URL endpoint for image uploads. If not specified, the default upload URL from the config file will be used.
uploadUrl (Default: null)
Note: This field gives you freedom to handle image uploads yourself. You are responsible for creating your own upload endpoint that handles file validation, storage, and returns the appropriate response format. This design allows you to implement your own business logic, security measures, and storage solutions (local filesystem, S3, cloud storage, etc.).
This field uses CKEditor's Custom Upload Adapter, which requires your upload endpoint to return a JSON response containing the uploaded image URL(s).
Expected Response Format:
Your upload endpoint must return a JSON response with one of the following formats:
Single image response:
{
"url": "https://example.com/uploads/image.jpg"
}
Responsive images response:
{
"urls": {
"default": "https://example.com/uploads/image.jpg",
"500": "https://example.com/uploads/image1.jpg",
"1000": "https://example.com/uploads/image2.jpg"
}
}
Example Laravel Controller:
use Illuminate\Http\Request; public function uploadImage(Request $request) { $request->validate([ 'upload' => 'required|image|max:2048', ]); $path = $request->file('upload')->store('uploads', 'public'); $url = asset('storage/' . $path); return response()->json([ 'url' => $url ]); }
For more details, see the CKEditor Custom Upload Adapter documentation.
name(string $name)
Sets the name of the field. This will be used as the form field name.
name (Default: 'ckeditor')
placeholder(string $placeholder)
Sets the placeholder text displayed in the editor when it's empty.
placeholder (Default: 'Type or paste your content here...')
height(string | Closure | null $height)
Fixes the editing area to the given CSS height, scrolling internally once content outgrows it. Without it the editor grows with its content.
CKEditor::make('content') ->height('400px')
minHeight(string | Closure | null $minHeight)
Lets the editing area start at the given CSS height while still growing with its content.
CKEditor::make('content') ->minHeight('10rem')
editorOptions(array | Closure $options)
Merges options over the resolved editor configuration for this field. See How options merge. Can be called more than once, with later calls taking precedence.
CKEditor::make('content') ->editorOptions(['menuBar' => ['isVisible' => false]])
disablePlugins(array | Closure $plugins)
Removes plugins from the resolved plugin list, switching those features off. Toolbar items belonging to a removed plugin are dropped along with it.
CKEditor::make('content') ->disablePlugins(['FontColor', 'FontBackgroundColor', 'Highlight'])
enablePlugins(array | Closure $plugins)
Re-enables plugins that the config file or a configureUsing callback disabled.
CKEditor::make('content') ->enablePlugins(['Highlight'])
disableToolbarItems(array | Closure $items)
Removes items from the toolbar, leaving the underlying plugins active. Orphaned separators are dropped automatically.
CKEditor::make('content') ->disableToolbarItems(['insertTable', 'codeBlock'])
Cleaning up removed images
Uploaded images stay in storage when they are later deleted from the editor. That is deliberate while editing: undo and redo need the file to still exist, so the earliest safe moment to reconcile storage is when the record is saved.
CKEditor::findRemovedImages() compares the previously saved document with the one about to be saved and returns the image URLs that disappeared:
use Kahusoftware\FilamentCkeditorField\CKEditor; $removed = CKEditor::findRemovedImages($oldHtml, $newHtml, urlPrefix: '/storage/uploads/');
The package never deletes files itself, mirroring how uploads work: you own the endpoint, so you own the cleanup. The optional urlPrefix restricts the result to your own uploads, so external or hotlinked images can never end up on a deletion list.
In a panel resource, capture the removed URLs before saving and delete after the save succeeds:
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; use Kahusoftware\FilamentCkeditorField\CKEditor; class EditPost extends EditRecord { protected static string $resource = PostResource::class; /** @var array<int, string> */ protected array $removedImages = []; protected function mutateFormDataBeforeSave(array $data): array { $this->removedImages = CKEditor::findRemovedImages( $this->record->getOriginal('content'), $data['content'] ?? null, urlPrefix: asset('storage/uploads'), ); return $data; } protected function afterSave(): void { foreach ($this->removedImages as $url) { Storage::disk('public')->delete(Str::after($url, '/storage/')); } } }
In a standalone Livewire component the same shape applies:
public function save(): void { $data = $this->form->getState(); $removed = CKEditor::findRemovedImages( $this->post->getOriginal('content'), $data['content'] ?? null, urlPrefix: asset('storage/uploads'), ); $this->post->update($data); foreach ($removed as $url) { Storage::disk('public')->delete(Str::after($url, '/storage/')); } }
Deleting only after a successful save keeps the files available if validation or the save itself fails.
Inherited field methods
The field extends Filament's base Field, so everything a standard form field
supports works here without package code, including:
CKEditor::make('content') ->label('Body') ->autofocus() // focuses the editor once it has initialised ->required() ->disabled() ->hidden() ->helperText('Shown under the field') ->columnSpanFull()
See the Filament form field documentation for the full list.
Testing
composer test
The test suite uses PestPHP and includes unit tests for field instantiation, method chaining, and configuration, as well as feature tests for rendering the field within Livewire components.
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please email hello@kahusoftware.com any security vulnerabilities to ensure they're promptly addressed.
Credits
License
The MIT License (MIT). Please see License File for more information.
* This open-source plugin is not affiliated with, endorsed, or sponsored by CKSource, and any references to CKEditor are solely for descriptive purposes under their respective copyrights and trademarks.
We do encourage you to check out CKEditor's premium features for your own implementation of CKEditor as the developers have worked hard to bring us a wonderful rich editor.