amidesfahani/filament-tinyeditor
A TinyMCE rich text editor integration for Filament Admin and Forms. Supports Filament 5.x, 4.x, and 3.x, with configurable profiles, toolbars, RTL direction, image and file attachments, custom external plugins, and resizable editing.
1. Match the package major to your Filament major — do this before anything else.
| Your stack | Install |
|---|---|
| Filament 5 / Laravel 12 / PHP 8.2+ | composer require amidesfahani/filament-tinyeditor:^5.0 |
| Filament 4 / Laravel 11 / PHP 8.2+ | composer require amidesfahani/filament-tinyeditor:^4.0 |
| Filament 3 / Laravel 10+ / PHP 8.1+ | composer require amidesfahani/filament-tinyeditor:^3.0 |
The package is a thin wrapper around a bundled TinyMCE build, so the version matrix is strict — there is no cross-major compatibility shim.
2. Publish assets (the step people skip and then debug for an hour).
php artisan vendor:publish --provider="AmidEsfahani\FilamentTinyEditor\TinyeditorServiceProvider"
php artisan vendor:publish --provider="AmidEsfahani\FilamentTinyEditor\TinyeditorServiceProvider" --tag="config"
php artisan vendor:publish --provider="AmidEsfahani\FilamentTinyEditor\TinyeditorServiceProvider" --tag="views"
php artisan vendor:publish --provider="AmidEsfahani\FilamentTinyEditor\TinyeditorServiceProvider" --tag="public"
If you skip the public tag you get a rendered textarea shell with no toolbar and tinymce is not defined in the console. The provider-level publish (no tag) covers everything, so in practice one command plus --tag="config" is enough.
3. First field — replace your RichEditor:
use AmidEsfahani\FilamentTinyEditor\TinyEditor;
TinyEditor::make('content')
->profile('simple')
->fileAttachmentsDisk('public')
->fileAttachmentsVisibility('public')
->fileAttachmentsDirectory('uploads/posts')
->columnSpanFull()
->required();
Run php artisan storage:link first, or inline images will 404.
Where to look first:
config/filament-tinyeditor.php — the profiles array is the real API surface. Everything else is a shortcut into it.resources/views/tiny-editor.blade.php (published) — this is where the Alpine x-load / init directive lives; most integration bugs are visible here.src/TinyEditor.php — since the README is terse, read the class for the full method list (setCustomConfigs, content_style, text patterns, dark mode, license key). It extends Filament's Field, so columnSpan, required, dehydrated, rules, helperText, disabled all behave normally.Validate the install: open a form, confirm /js/tinymce/... (or the configured CDN) loads with 200s, type, add an image, save, and confirm the HTML round-trips into the DB.
Profile-per-field instead of one global editor. Treat profiles as content contracts: minimal for short marketing blurbs, simple for comments/bio fields, default for article bodies, full for pages needing tables/media/anchors, none to drop the toolbar entirely. Pipe groups in the config let you visually separate tool clusters without writing custom JS:
'default' => [
'plugins' => 'accordion autoresize directionality advlist link image lists code fullscreen media table emoticons',
'toolbar' => 'undo redo removeformat | styles | bold italic | rtl ltr | alignjustify alignright aligncenter alignleft | numlist bullist outdent indent | blockquote table hr | image link media codesample emoticons | wordcount fullscreen',
'upload_directory' => null,
'custom_configs' => ['paste_webkit_styles' => 'color font-size'],
],
Use ->profile('custom') and add a custom key to profiles when you need one field that doesn't match any built-in.
Centralise field configuration. Because every resource would otherwise repeat disk/visibility/directory/profile, expose a static factory in a support class:
final class Editors
{
public static function body(string $name = 'content'): TinyEditor
{
return TinyEditor::make($name)
->profile('default')
->fileAttachmentsDisk(config('filesystems.default'))
->fileAttachmentsVisibility('public')
->fileAttachmentsDirectory('cms/'.tenant('id').'/body')
->columnSpanFull();
}
}
This is the single place you change when you migrate from local public to S3, or when you switch the disk to a private CDN-backed one.
Media handling. Three knobs, and they must agree: fileAttachmentsDisk() (Laravel disk name), fileAttachmentsVisibility() (must be public for inline <img> URLs to resolve), and fileAttachmentsDirectory() (per-profile upload_directory is the default when the field doesn't set one). For S3/R2, set visibility public and configure the disk's url so TinyMCE's image src points at the CDN, not s3.amazonaws.com.
RTL / multilingual. Two layers: the global 'direction' => 'rtl' config key for a whole Arabic/Hebrew admin panel, and ->rtl() / ->direction('auto|rtl|ltr') per field for mixed-language forms. Either way, directionality must be in the profile's plugins and rtl ltr in the toolbar, otherwise alignment buttons do nothing useful.
External plugins. Register third-party TinyMCE plugins by URL in the profile, then reference their buttons in toolbar:
'external_plugins' => [
'paste_from_word' => 'https://unpkg.com/@pangaeatech/tinymce-paste-from-word-plugin@latest/index.js',
],
For anything intranet/offline, self-host the plugin file under public/ and point at a relative path — @latest from unpkg is a supply-chain and availability dependency.
Runtime custom configs. Beyond the config file, the field exposes a runtime custom-config setter (per the 2.0.9 "set custom config" change) plus content_style, which is the correct way to style the editor iframe body (fonts, line-height, code block background) so authored content matches front-end rendering. Confirm the exact method name against src/TinyEditor.php — the README doesn't document it.
Output side. The field stores raw HTML. In tables use TextColumn::make('content')->html()->limit(80) or strip tags; on the public site run the HTML through a sanitizer (HTMLPurifier / mews/purifier) and then render unescaped. Never {!! !!} untrusted editor output directly.
Testing. Livewire fillForm(['content' => '<p>hi</p>']) exercises the state contract fine, but it does not exercise TinyMCE. Anything involving toolbar behaviour, image upload or modal interaction needs Dusk/browser coverage; keep one smoke test that asserts the editor iframe exists.
--tag="public" publishes, a stale public/js/tinymce after a package upgrade, or a npm run build that wipes the vendor-published directory all produce a silent, toolbar-less editor. Re-publish and php artisan view:clear after every upgrade.^4.0 on Filament 3 won't necessarily fail loudly at install time; it fails at render. Pin the minor ("^4.0.8") so you get the temp-upload filename fix and the modal-overlap fix rather than an early 4.0.x.fileAttachmentsVisibility('private') with inline <img> tags gives you broken images or signed-URL churn. Public visibility, or a CDN disk with a stable url, is the only workable combination for a WYSIWYG.->hidden() toggles, and tab switches still re-mount Alpine components. Keep the field dehydrated, give repeater rows stable keys, and test drag-reorder explicitly if you use it inside a Repeater.mso- classes and sometimes base64 images. Set paste_webkit_styles, consider valid_elements/invalid_elements via custom_configs, and always sanitize server-side — ->rules(['max:65535']) won't stop <script>.->rtl(). The config key wins at boot for the default direction; per-field calls override the instance. If a field stubbornly ignores ->direction(), check whether directionality is actually loaded in that profile.dark_mode config (auto|media|class|force|false) toggles TinyMCE's own skin, but your app's CSS variables do not cross into the editor iframe. Use content_style with explicit colors if editor content must match a dark front-end.config/filament-tinyeditor.php requires php artisan config:clear locally and a redeploy/config rebuild in production. Profile edits that "don't apply" are almost always this.ax-load instead of x-load is a known historical breakage.composer.json so a surprise minor release can't move under you. The published views and config are your supported extension points — prefer overriding them over patching the vendor tree.window.tinymce in console → 404s on /js/tinymce/* → inspect the rendered x-load/x-load-src attributes → check whether the field is inside a hidden container at init time → re-publish views → clear caches. That order resolves the large majority of reports.How can I help you explore Laravel packages today?