Download the PHP package asignua/filament-rich-editor-toolkit without Composer
On this page you can find all versions of the php package asignua/filament-rich-editor-toolkit. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download asignua/filament-rich-editor-toolkit
More information about asignua/filament-rich-editor-toolkit
Files in asignua/filament-rich-editor-toolkit
Package filament-rich-editor-toolkit
Short Description Five independent plugins for the Filament 5 RichEditor (TipTap): PasteCleanPlugin removes Word and Google Docs junk (mso-* styles, fonts, colour spans), CustomAttributesPlugin keeps class, id, style and data-* attributes both in the browser and through the server sanitizer, EmbedPlugin allows safe iframe and video embeds from an allow-list, ImageUrlPlugin inserts an image by URL, and StickyToolbarPlugin keeps the toolbar visible on long content, even inside sections, tabs and repeaters.
License MIT
Homepage https://github.com/asignua/filament-rich-editor-toolkit
Informations about the package filament-rich-editor-toolkit
Filament Rich Editor Toolkit
Five free plugins for the Filament 5 RichEditor (TipTap) that fix the problems people keep
reporting in Filament's GitHub Help:
| Plugin | Fixes |
|---|---|
PasteCleanPlugin |
Pasting from Word / Google Docs drags in colours, fonts, mso-* junk and <span style> soup. |
CustomAttributesPlugin |
class, id, style and data-* written in the source view are silently stripped, in the browser and on the server. |
EmbedPlugin |
<iframe> / video embeds are removed by the schema and the sanitizer. |
ImageUrlPlugin |
No way to insert an image that lives on another site. |
StickyToolbarPlugin |
The toolbar scrolls away on long content (#17391, #16919, #11685). |
Each plugin works alone; take only what you need.
- Screenshots
- Requirements
- Installation
- Usage
- Rendering on the front end
- Configuration
- Gotchas
- Translations
- AI agents
- Testing
Screenshots



Requirements
- PHP 8.3+
- Filament 5
- Laravel 12 or 13
Installation
filament:assets publishes the compiled editor modules. They are loadedOnRequest(): no <script> is printed on a page
without a rich editor, the editor imports them by URL.
Optionally publish the config and the translations:
Usage
A toolbar button is shown only when it is listed in toolbarButtons() (cleanFormat, embed, imageUrl); an unknown
name makes Filament throw, so list only the plugins you installed.
PasteCleanPlugin
On Ctrl+V the structure stays (paragraphs, headings, lists, tables, links, bold/italic/underline) and colours, font
sizes, backgrounds and every class/id/style they carry disappear. A pasted link keeps its href only for
http(s):, mailto: and tel:; relative paths and #anchors (Word's #_Toc…, local file paths) become plain text. A paste from inside an editor is never
touched (the data-pm-slice cutoff), so it composes with CustomAttributesPlugin. The marker is ProseMirror's, not
the toolkit's: a paste copied from any ProseMirror/TipTap-based app (Confluence, GitLab, another CMS) skips the
cleanup too and keeps its class/style.
The cleanFormat button runs the same cleanup on the selected fragment of existing content. Unlike Filament's
clearFormatting it keeps headings and lists (that one runs clearNodes().unsetAllMarks()). What the editor itself
put there survives untouched: custom blocks, embeds (<iframe>), images (also ones inserted by URL or stored on another
host), code blocks, mentions and merge tags, grid, details and the lead paragraph. Every link stays too (relative
paths, #anchors, tel:, and a link's target/rel), except the script schemes
javascript:, vbscript:, data: and file:. The result goes back like a paste, so a few words picked out of a
paragraph stay in it, and a table cell selection is left alone. A prefix you list is kept whatever its scheme:
Images pasted from Word and Google Docs are removed (Filament uploads only data: images from the clipboard); add them
through the upload button.
CustomAttributesPlugin
Keeps class, id, style and further attributes you list, on paragraphs, headings, lists, tables, images, links,
details, grid, spans and generic <div> containers. A name the node declares itself belongs to the node and wins: an
<img> keeps class and style but not id (Filament's image node owns id as its media key, stored as
data-id), and an extra name such as title is not kept on a node that has its own title (image, iframe). It also stops legacy markup from collapsing: <div class="row"><img> <span>…</span></div> keeps its direct children, and <li><div>…</div></li> stays a list item. A <div> that wraps an <iframe> (a responsive embed wrapper) keeps it inside. A style/class that another extension writes itself is dropped only on its own tag (text-align on paragraphs and headings, width/height on images, lead and the grid classes on div); on a <td>, <table>, <iframe> or <p class="lead"> it is yours and stays.
The list is explicit because Symfony's HTML sanitizer, which Filament runs over every rendered rich text, cannot allow
data-* as a pattern. on* handlers, URL attributes (href, src, srcset, srcdoc, action, formaction,
ping, poster, ...), is, any name with : and framework directives (x-*, hx-*, v-*, ng-*, wire:*) are
never accepted: the panel runs Alpine over the editor, so an allowed x-init would be stored XSS against every admin.
That list is a backstop, not a whitelist; allow only what you need (data-*, aria-*, role, title).
->attributes([...]) / ->types([...]) on a plugin instance replace the config for that field only. A front-end request
never builds the form, so pass the same lists to the renderer, or the values are saved and dropped on the page:
EmbedPlugin
The "Embed" button takes a YouTube / Vimeo link (including youtu.be, Shorts, timecodes) or an https address from your
allow-list, and inserts a bare <iframe>. A video link is rebuilt from provider and id
(youtube-nocookie.com, no autoplay; an unlisted Vimeo video keeps its privacy hash), never copied. Pasted or
hand-written iframes pass the same check at once in the browser and when the HTML is parsed on the server; an iframe from
any other host does not exist as a node and is dropped. The check runs again when a node is rendered, so JSON content
(RichEditor::json(), an array handed to the renderer) cannot smuggle one in either. The same goes for the admin editor: an iframe node whose src fails the list is drawn as an inert placeholder, and the configured sandbox, allow and referrerpolicy are forced there too. YouTube's own embed code (www.youtube.com/embed/ID) and watch?v= / youtu.be / vimeo.com iframes are rebuilt to the youtube-nocookie.com / player.vimeo.com form rather than dropped. A host/path entry matches on a
segment boundary (the host is case-insensitive, the path is not), and a path with dot segments (/maps/embed/../../url), %2e, %2f or a backslash is refused.
sandbox, allow, referrerpolicy and loading="lazy" are forced onto every embed on render, whatever the stored HTML
says.
ImageUrlPlugin
Inserts an <img> that is loaded from the other site, not copied. Only absolute http(s) URLs are accepted;
ImageUrlPlugin::make()->hosts(['cdn.example.com']) restricts what the dialog accepts. It is not a content filter:
an <img> typed in the source view or pasted is not checked against it. http:// is accepted (mixed content on an
https site).
StickyToolbarPlugin
A panel plugin, CSS only:
Filament 5 has RichEditor::stickyToolbar() per field; this makes it the default for every editor and fixes the case
where it silently does nothing, because a section, tab or repeater item clips with overflow: hidden (which turns it
into a scroll container that position: sticky cannot escape). Those containers are switched to overflow: clip.
Run php artisan filament:assets so the stylesheet is published.
Offset precedence: a field's own ->stickyOffset(), then the plugin's offset(), then Filament's own default (the topbar, or inside a modal the height of its sticky header); the plugin offset does not apply inside a modal.
Opt a single field out with RichEditor::make('body')->withoutStickyToolbar() (it sets data-toolkit-sticky="off", which the stylesheet respects, even over Filament's native ->stickyToolbar()).
Rendering on the front end
RichContentRenderer parses the stored HTML with the same tiptap-php schema and then sanitizes it, and both steps drop
what they do not know. Use the helper:
or attach the plugins to a renderer you build yourself and render it through the toolkit:
This adds CustomAttributesPlugin and EmbedPlugin and sanitizes with the toolkit's own sanitizer: a copy of
Filament's config plus your attribute allow-list and, when EmbedPlugin is attached, <iframe> whose src passes the
embed allow-list. Filament's shared sanitizer is never changed, so TextColumn::html(), TextEntry::html(),
notifications and the Markdown editor keep Filament's defaults. A renderer's own toHtml() uses that shared sanitizer,
which is why RichEditorToolkit::toHtml($renderer) is needed (the same goes for HasRichContent models:
RichEditorToolkit::toHtml($post->getRichContentAttribute('body')->getRenderer())). Do not add PasteCleanPlugin
to the renderer: the cleanup happens once, at paste time.
Configuration
See config/rich-editor-toolkit.php: attributes, types, embed, paste_clean,
sticky_toolbar.
Gotchas
- Both halves, always.
CustomAttributesPluginandEmbedPluginmust be in the editor and in the renderer. Miss the renderer and the data sits in the database and vanishes on the page. - Publish the assets. A missing
filament:assetsmeans a 404 for the module URL and aconsole.errorfrom Filament's loader. Nothing else fails, the plugin is just absent. - Node types. An attribute is kept only on the node types in the list (
Extensions\CustomAttributes::TYPESplus thetypesconfig). The PHP and the JS lists are identical on purpose; a test pins it. - A node's own attribute wins.
idon an<img>and any listed name a node declares itself (titleon image/iframe,width,src, ...) follow that node's rules, not the toolkit's; put the anchor on a wrapping element. - The sanitizer is the toolkit's own. Allowances apply only to
RichEditorToolkit::renderer()andRichEditorToolkit::toHtml(); a plainRichContentRenderer::toHtml()and every->html()column still drop<iframe>and the extra attributes. toolbarButtons(). ListcleanFormat,embed,imageUrlonly when the matching plugin is registered.
Translations
English, Ukrainian, German, Spanish, French, Italian, Dutch, Polish, Brazilian Portuguese and Turkish. The namespace is
rich-editor-toolkit::.
AI agents
Laravel Boost guidelines ship in resources/boost/guidelines/core.blade.php.
Testing
Changelog and license
All versions of filament-rich-editor-toolkit with dependencies
filament/filament Version ^5.0
illuminate/contracts Version ^12.0|^13.0
spatie/laravel-package-tools Version ^1.16
symfony/html-sanitizer Version ^7.0|^8.0
ueberdosis/tiptap-php Version ^2.0