Customize image html or attributes in RichContentRenderer

Hey! I'm basically trying to lazy-load images in RichEditor content. I tried extending RichContentRenderer and also with "mixin" (like awcodes show me), but I'm not sure if it's even possible... Don't have I any other way other than writing my own node? I don't want to lose the features included in the default Image node...
40 Replies
awcodes
awcodes2mo ago
Might be better to add the lazy load attribute to the Core Image extension with a PR.
charlie
charlieOP2mo ago
sure so you think it's possible? I hope it's not blocked in tiptap-php Image Node
awcodes
awcodes2mo ago
We extend the Tiptap extension. Same thing I did in my plugin.
charlie
charlieOP2mo ago
of course... I need to understand why I can't see my custom attribute I believe we should be able to pass any attribute to any node, or wrap in any html... because there are a million different use cases don't you agree?
awcodes
awcodes2mo ago
if only prosemirror worked that way
charlie
charlieOP2mo ago
mmh... but there is an addAttributes() method in ImageExtension.php, so it should be possible to have an API around this, as well in some others Extensions, right?
awcodes
awcodes2mo ago
it's hard to make it global though, because the attributes need to be specific to the node or mark so they are processed correctly
charlie
charlieOP2mo ago
GitHub
Add a method to lazy load RichEditor images by CharlieEtienne · Pu...
Description This PR adds a new lazyLoadImages method to RichContentRenderer. use Filament\Forms\Components\RichEditor\RichContentRenderer; RichContentRenderer::make($record->content) -&...
awcodes
awcodes2mo ago
Looks good. I would be hesitant to do it as an all or nothing though since images that appear above the fold should not be set to lazy load according to the html specs. 🤔
charlie
charlieOP2mo ago
I agree, but what are the alternatives?
awcodes
awcodes2mo ago
Add it as an attribute to the image extension and add a toggle in the modal action to set it. Unfortunately there’s a bug at the moment with the merging in core, so you can’t override from an external extension. Working on that.
charlie
charlieOP2mo ago
there’s a bug at the moment with the merging in core
What do you mean? When I dump attributes here, I have my attributes.
// Components/RichEditor/TipTapExtensions/ImageExtension.php

public function addAttributes(): array
{
return [
...parent::addAttributes(),
'id' => [
'parseHTML' => fn ($DOMNode) => $DOMNode->getAttribute('data-id') ?: null,
'renderHTML' => function ($attributes) {
dump($attributes);
return ['data-id' => $attributes->id ?? null];
},
],
];
}
// Components/RichEditor/TipTapExtensions/ImageExtension.php

public function addAttributes(): array
{
return [
...parent::addAttributes(),
'id' => [
'parseHTML' => fn ($DOMNode) => $DOMNode->getAttribute('data-id') ?: null,
'renderHTML' => function ($attributes) {
dump($attributes);
return ['data-id' => $attributes->id ?? null];
},
],
];
}
But on the next step, they disappear and are not merged. They are not passed in vendor/ueberdosis/tiptap-php/src/Core/DOMSerializer.php Is this what you're talking about?
awcodes
awcodes2mo ago
Sorry, maybe I’m lost. What do you mean by 'next step'? The bug i'm referring to is on the JS side
charlie
charlieOP2mo ago
oh ok. I'm working on a PR suggested by Dan to be able to do that:
use Filament\Forms\Components\RichEditor\RichContentRenderer;

RichContentRenderer::make($record->content)
->processNodesUsing(function (object &$node): void {
if ($node->type !== 'image') {
return;
}

$node->attrs->loading = 'lazy';
})
->toHtml()
use Filament\Forms\Components\RichEditor\RichContentRenderer;

RichContentRenderer::make($record->content)
->processNodesUsing(function (object &$node): void {
if ($node->type !== 'image') {
return;
}

$node->attrs->loading = 'lazy';
})
->toHtml()
https://github.com/filamentphp/filament/pull/17526 But I can't find a clean way to pass node attributes to extensions, without having to modify each extension
awcodes
awcodes2mo ago
the attributes should be handled by their own extension not by the renderer. again i may be misunderstanding ie, extension-image.js and ImageExtension.php should handle the lazy loading attribute then the renderer will automatically handle it
charlie
charlieOP2mo ago
on my first PR, Dan told the following:
We could add endless methods to customize the output like this, so maybe we just need a method where you can tap into the editor
https://github.com/filamentphp/filament/pull/17507#issuecomment-3221738582 So I'm trying to do that. But I'm completely lost I think 🙂
awcodes
awcodes2mo ago
ok. maybe we're talking about 2 different things. lol i see what he's thinking, but that's not quite the same as being able to set these kind of things in the editor js instance, like having some images lazy and other not.
charlie
charlieOP2mo ago
To clarify, I was trying to add any attribute to any node. yeah, that's another topic, of course
awcodes
awcodes2mo ago
i'm also not 100% sure Tiptap-PHP allows for setting arbitrary attrs on the object. ie, the attrs are read from the data
charlie
charlieOP2mo ago
it's possible. For example, if I do that:
public function getTipTapPhpExtensions(): array
{
return [
app(Blockquote::class, ['options' => ['HTMLAttributes' => ['customAttr' => 'customValue']]]),
app(Bold::class),
app(BulletList::class),
// ...
public function getTipTapPhpExtensions(): array
{
return [
app(Blockquote::class, ['options' => ['HTMLAttributes' => ['customAttr' => 'customValue']]]),
app(Bold::class),
app(BulletList::class),
// ...
it's showing in toUnsafeHtml() It works also when adding some attributes here :
$editor->configuration['extensions'] = [...]
$editor->configuration['extensions'] = [...]
And also from extension, if I add the attributes keys in addAttributes method.
awcodes
awcodes2mo ago
Could it just be a rule on the sanitizer stripping it from the html?
charlie
charlieOP2mo ago
of course, it's striped from html thanks to the sanitizer. But not loading="lazy". That's not the problem. My goal at the moment is to be able to set any dynamic attribute to a node, and pass it to the extension to have it in HTML. (maybe that's a bad idea though 🙂 )
awcodes
awcodes2mo ago
looks like 'lazy' isn't an allowed attribute in the sanitizer's 'allowSafeElements()', try adding this to the sanitizer config in the support/src/SupportServiceProvider.php
->allowAttribute('lazy', allowedElements: 'img')
->allowAttribute('lazy', allowedElements: 'img')
charlie
charlieOP2mo ago
no I'm sure it's allowed
awcodes
awcodes2mo ago
ok
charlie
charlieOP2mo ago
it works when I add it in the ImageExtension
awcodes
awcodes2mo ago
let me play around with it. you're PR code looks ok to me. wonder if it could be a conflict with the processFileAttachments method since it's also acting against images wouldn't think it would be a problem though
charlie
charlieOP2mo ago
I don't think so because it doesn't work if I do that:
->processNodesUsing(function (object &$node) {
if ($node->type !== 'paragraph') {
return;
}

$node->attrs->class = 'not-prose';
})
->processNodesUsing(function (object &$node) {
if ($node->type !== 'paragraph') {
return;
}

$node->attrs->class = 'not-prose';
})
awcodes
awcodes2mo ago
ok, i think the problem is that if an attribute doesn't exist in the php version of the extension then it's just going to get stripped out regardless. It only works with this in ImageExtension.php:
public function addAttributes(): array
{
return [
...parent::addAttributes(),
'id' => [
'parseHTML' => fn ($DOMNode) => $DOMNode->getAttribute('data-id') ?: null,
'renderHTML' => fn ($attributes) => [
'data-id' => $attributes->id ?? null,
'loading' => $attributes->loading ?? null,
],
],
];
}
public function addAttributes(): array
{
return [
...parent::addAttributes(),
'id' => [
'parseHTML' => fn ($DOMNode) => $DOMNode->getAttribute('data-id') ?: null,
'renderHTML' => fn ($attributes) => [
'data-id' => $attributes->id ?? null,
'loading' => $attributes->loading ?? null,
],
],
];
}
awcodes
awcodes2mo ago
No description
charlie
charlieOP2mo ago
yes, exactly
awcodes
awcodes2mo ago
technically this would be the code. sorry:
public function addAttributes(): array
{
return [
...parent::addAttributes(),
'id' => [
'parseHTML' => fn ($DOMNode) => $DOMNode->getAttribute('data-id') ?: null,
'renderHTML' => fn ($attributes) => [
'data-id' => $attributes->id ?? null,
],
],
'loading' => [
'default' => null,
'parseHTML' => fn ($DOMNode) => $DOMNode->getAttribute('loading') ?: null,
'renderHTML' => fn ($attributes) => [
'loading' => $attributes->loading ?? null,
],
],
];
}
public function addAttributes(): array
{
return [
...parent::addAttributes(),
'id' => [
'parseHTML' => fn ($DOMNode) => $DOMNode->getAttribute('data-id') ?: null,
'renderHTML' => fn ($attributes) => [
'data-id' => $attributes->id ?? null,
],
],
'loading' => [
'default' => null,
'parseHTML' => fn ($DOMNode) => $DOMNode->getAttribute('loading') ?: null,
'renderHTML' => fn ($attributes) => [
'loading' => $attributes->loading ?? null,
],
],
];
}
but, yea, i don't know that there's anything we can do on our side since that logic is handle inside the tiptap php package it's trying to mimic the tiptap js, which requires the attributes to be specifically defined due to prosemirror's restrictions.
charlie
charlieOP2mo ago
but we can pass options to extensions like that:
app(TextAlign::class, [
'options' => [
'types' => ['heading', 'paragraph'],
'alignments' => ['start', 'center', 'end', 'justify'],
'defaultAlignment' => 'start',
],
]),
app(TextAlign::class, [
'options' => [
'types' => ['heading', 'paragraph'],
'alignments' => ['start', 'center', 'end', 'justify'],
'defaultAlignment' => 'start',
],
]),
awcodes
awcodes2mo ago
options and attributes aren't the same thing.
charlie
charlieOP2mo ago
in options, I believe if you have an array of attributes in 'HTMLAttributes' it works
app(TextAlign::class, [
'options' => [
'types' => ['heading', 'paragraph'],
'alignments' => ['start', 'center', 'end', 'justify'],
'defaultAlignment' => 'start',
'HTMLAttributes' => ['loading' => 'lazy'],
],
]),
app(TextAlign::class, [
'options' => [
'types' => ['heading', 'paragraph'],
'alignments' => ['start', 'center', 'end', 'justify'],
'defaultAlignment' => 'start',
'HTMLAttributes' => ['loading' => 'lazy'],
],
]),
awcodes
awcodes2mo ago
you could do it that way, but i would expect that to force the image to always have the loading attribute, the attribute would still need to be defined in the getAttributes(), for the merging to work that way.
charlie
charlieOP2mo ago
sure, it will force it...
awcodes
awcodes2mo ago
basically, though, i guess what i'm getting at, is that in tiptap php any attribute that can be dynamically set has to be registered as an attribute explicitly.
charlie
charlieOP2mo ago
yeah, either in addAttributes or addOptions
awcodes
awcodes2mo ago
I think the PR is still good though. Opens up some possibilities. Just won’t work with existing extensions unless those attrs are also added to the core js/php extensions. But the reason Dan is pulling them out of the container is so you can bind in your own version of the php extension as a replacement. So if you didn’t want loading in the core extension you could just copy it and add whatever you need then bind it in a service provider.

Did you find this page helpful?