Issue #3247795 by heddn, Graber, ravi.shankar, yogeshmpawar, Anchal_gupta, Wim Leers, Fabianx, alexpott, benmorss, catch: Add text filter plugin to support <img loading="lazy"> and remove it from editor_file_reference
parent
bd4448dbdb
commit
a266ba6e62
|
@ -543,7 +543,7 @@ JS;
|
|||
$uploaded_image = File::load(1);
|
||||
$image_url = $this->container->get('file_url_generator')->generateString($uploaded_image->getFileUri());
|
||||
$image_uuid = $uploaded_image->uuid();
|
||||
$assert_session->elementExists('xpath', sprintf('//img[@src="%s" and @loading="lazy" and @width and @height and @data-entity-uuid="%s" and @data-entity-type="file"]', $image_url, $image_uuid));
|
||||
$assert_session->elementExists('xpath', sprintf('//img[@src="%s" and @width and @height and @data-entity-uuid="%s" and @data-entity-type="file"]', $image_url, $image_uuid));
|
||||
|
||||
// Ensure that width, height, and length attributes are not stored in the
|
||||
// database.
|
||||
|
@ -555,7 +555,7 @@ JS;
|
|||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ck-editor'));
|
||||
$page->pressButton('Save');
|
||||
|
||||
$assert_session->elementExists('xpath', sprintf('//img[@src="%s" and @loading="lazy" and @width and @height and @data-entity-uuid="%s" and @data-entity-type="file"]', $image_url, $image_uuid));
|
||||
$assert_session->elementExists('xpath', sprintf('//img[@src="%s" and @width and @height and @data-entity-uuid="%s" and @data-entity-type="file"]', $image_url, $image_uuid));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,6 +5,10 @@
|
|||
* Post update functions for Editor.
|
||||
*/
|
||||
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\filter\FilterFormatInterface;
|
||||
use Drupal\filter\FilterPluginCollection;
|
||||
|
||||
/**
|
||||
* Implements hook_removed_post_updates().
|
||||
*/
|
||||
|
@ -13,3 +17,30 @@ function editor_removed_post_updates() {
|
|||
'editor_post_update_clear_cache_for_file_reference_filter' => '9.0.0',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable filter_image_lazy_load if editor_file_reference is enabled.
|
||||
*/
|
||||
function editor_post_update_image_lazy_load(): void {
|
||||
if (\Drupal::service('plugin.manager.filter')->hasDefinition('editor_file_reference')) {
|
||||
foreach (FilterFormat::loadMultiple() as $format) {
|
||||
assert($format instanceof FilterFormatInterface);
|
||||
$collection = $format->filters();
|
||||
$configuration = $collection->getConfiguration();
|
||||
assert($collection instanceof FilterPluginCollection);
|
||||
if (array_key_exists('editor_file_reference', $configuration)) {
|
||||
$collection->addInstanceId('filter_image_lazy_load');
|
||||
$configuration['filter_image_lazy_load'] = [
|
||||
'id' => 'filter_image_lazy_load',
|
||||
'provider' => 'editor',
|
||||
'status' => TRUE,
|
||||
// Place lazy loading after editor file reference.
|
||||
'weight' => $configuration['editor_file_reference']['weight'] + 1,
|
||||
'settings' => [],
|
||||
];
|
||||
$collection->setConfiguration($configuration);
|
||||
$format->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
|
|||
* @Filter(
|
||||
* id = "editor_file_reference",
|
||||
* title = @Translation("Track images uploaded via a Text Editor"),
|
||||
* description = @Translation("Ensures that the latest versions of images uploaded via a Text Editor are displayed."),
|
||||
* description = @Translation("Ensures that the latest versions of images uploaded via a Text Editor are displayed, along with their dimensions."),
|
||||
* type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE
|
||||
* )
|
||||
*/
|
||||
|
@ -92,22 +92,16 @@ class EditorFileReference extends FilterBase implements ContainerFactoryPluginIn
|
|||
if ($file instanceof FileInterface) {
|
||||
$node->setAttribute('src', $file->createFileUrl());
|
||||
if ($node->nodeName == 'img') {
|
||||
// Without dimensions specified, layout shifts can occur,
|
||||
// which are more noticeable on pages that take some time to load.
|
||||
// As a result, only mark images as lazy load that have dimensions.
|
||||
$image = $this->imageFactory->get($file->getFileUri());
|
||||
$width = $image->getWidth();
|
||||
$height = $image->getHeight();
|
||||
if ($width !== NULL && $height !== NULL) {
|
||||
if (!$node->hasAttribute('width')) {
|
||||
$node->setAttribute('width', $width);
|
||||
}
|
||||
if (!$node->hasAttribute('height')) {
|
||||
$node->setAttribute('height', $height);
|
||||
}
|
||||
if (!$node->hasAttribute('loading')) {
|
||||
$node->setAttribute('loading', 'lazy');
|
||||
}
|
||||
// Set dimensions to avoid content layout shift (CLS).
|
||||
// @see https://web.dev/cls/
|
||||
if ($width !== NULL && !$node->hasAttribute('width')) {
|
||||
$node->setAttribute('width', (string) $width);
|
||||
}
|
||||
if ($height !== NULL && !$node->hasAttribute('height')) {
|
||||
$node->setAttribute('height', (string) $height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\editor\Functional\Update;
|
||||
|
||||
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
|
||||
|
||||
/**
|
||||
* Tests automatically adding editor_image_lazy_load filter to text formats
|
||||
* using editor_file_reference.
|
||||
*
|
||||
* @group Update
|
||||
*/
|
||||
class EditorAddLazyLoadImageFilterUpdateTest extends UpdatePathTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setDatabaseDumpFiles(): void {
|
||||
$this->databaseDumpFiles = [
|
||||
__DIR__ . '/../../../../../system/tests/fixtures/update/drupal-9.4.0.bare.standard.php.gz',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests upgrading filter settings.
|
||||
*
|
||||
* @see editor_post_update_image_lazy_load()
|
||||
*/
|
||||
public function testUpdateLazyImageLoad(): void {
|
||||
$config = $this->config('filter.format.full_html');
|
||||
$this->assertArrayNotHasKey('filter_image_lazy_load', $config->get('filters'));
|
||||
|
||||
$this->runUpdates();
|
||||
|
||||
$config = $this->config('filter.format.full_html');
|
||||
$filters = $config->get('filters');
|
||||
$this->assertArrayHasKey('filter_image_lazy_load', $filters);
|
||||
$this->assertEquals($filters['editor_file_reference']['weight'] + 1, $filters['filter_image_lazy_load']['weight']);
|
||||
}
|
||||
|
||||
}
|
|
@ -130,7 +130,7 @@ class EditorFileReferenceFilterTest extends KernelTestBase {
|
|||
$this->assertSame($expected_output, $output->getProcessedText());
|
||||
$this->assertEquals($cache_tag, $output->getCacheTags());
|
||||
|
||||
// Add a valid image for test lazy loading feature.
|
||||
// Add a valid image for image dimension testing.
|
||||
/** @var array stdClass */
|
||||
$files = $this->getTestFiles('image');
|
||||
$image = reset($files);
|
||||
|
@ -138,16 +138,16 @@ class EditorFileReferenceFilterTest extends KernelTestBase {
|
|||
[$width, $height] = getimagesize('public://llama.jpg');
|
||||
$dimensions = 'width="' . $width . '" height="' . $height . '"';
|
||||
|
||||
// Image dimensions and loading attributes are present.
|
||||
// Image dimensions are present.
|
||||
$input = '<img src="llama.jpg" data-entity-type="file" data-entity-uuid="' . $uuid . '" />';
|
||||
$expected_output = '<img src="/' . $this->siteDirectory . '/files/llama.jpg" data-entity-type="file" data-entity-uuid="' . $uuid . '" ' . $dimensions . ' loading="lazy" />';
|
||||
$expected_output = '<img src="/' . $this->siteDirectory . '/files/llama.jpg" data-entity-type="file" data-entity-uuid="' . $uuid . '" ' . $dimensions . ' />';
|
||||
$output = $test($input);
|
||||
$this->assertSame($expected_output, $output->getProcessedText());
|
||||
$this->assertEquals($cache_tag, $output->getCacheTags());
|
||||
|
||||
// Image dimensions and loading attributes are set manually.
|
||||
$input = '<img src="llama.jpg" data-entity-type="file" data-entity-uuid="' . $uuid . '"width="41" height="21" loading="eager" />';
|
||||
$expected_output = '<img src="/' . $this->siteDirectory . '/files/llama.jpg" data-entity-type="file" data-entity-uuid="' . $uuid . '" width="41" height="21" loading="eager" />';
|
||||
// Image dimensions are set manually.
|
||||
$input = '<img src="llama.jpg" data-entity-type="file" data-entity-uuid="' . $uuid . '"width="41" height="21" />';
|
||||
$expected_output = '<img src="/' . $this->siteDirectory . '/files/llama.jpg" data-entity-type="file" data-entity-uuid="' . $uuid . '" width="41" height="21" />';
|
||||
$output = $test($input);
|
||||
$this->assertSame($expected_output, $output->getProcessedText());
|
||||
$this->assertEquals($cache_tag, $output->getCacheTags());
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Drupal\filter\Plugin\Filter;
|
||||
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\filter\FilterProcessResult;
|
||||
use Drupal\filter\Plugin\FilterBase;
|
||||
|
||||
/**
|
||||
* Provides a filter to lazy load tracked images.
|
||||
*
|
||||
* @Filter(
|
||||
* id = "filter_image_lazy_load",
|
||||
* title = @Translation("Lazy load images"),
|
||||
* description = @Translation("Instruct browsers to lazy load images if dimensions are specified. Use in conjunction with and place after the 'Track images uploaded via a Text Editor' filter that adds image dimensions required for lazy loading. Results can be overridden by <code><img loading="eager"></code>."),
|
||||
* type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE,
|
||||
* weight = 15
|
||||
* )
|
||||
*/
|
||||
final class FilterImageLazyLoad extends FilterBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function process($text, $langcode): FilterProcessResult {
|
||||
$result = new FilterProcessResult($text);
|
||||
|
||||
// If there are no images, return early.
|
||||
if (stripos($text, '<img ') === FALSE && stripos($text, 'data-entity-type="file"') === FALSE) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
return $result->setProcessedText($this->transformImages($text));
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform markup of images to include loading="lazy".
|
||||
*
|
||||
* @param string $text
|
||||
* The markup to transform.
|
||||
*
|
||||
* @return string
|
||||
* The transformed text with loading attribute added.
|
||||
*/
|
||||
private function transformImages(string $text): string {
|
||||
$dom = Html::load($text);
|
||||
$xpath = new \DOMXPath($dom);
|
||||
// Only set loading="lazy" if no existing loading attribute is specified and
|
||||
// dimensions are specified.
|
||||
foreach ($xpath->query('//img[not(@loading="eager") and @width and @height]') as $element) {
|
||||
assert($element instanceof \DOMElement);
|
||||
$element->setAttribute('loading', 'lazy');
|
||||
}
|
||||
return Html::serialize($dom);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Drupal\Tests\filter\Unit;
|
||||
|
||||
use Drupal\filter\Plugin\Filter\FilterImageLazyLoad;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\filter\Plugin\Filter\FilterImageLazyLoad
|
||||
* @group editor
|
||||
*/
|
||||
final class FilterImageLazyLoadTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* @var \Drupal\filter\Plugin\Filter\FilterImageLazyLoad
|
||||
*/
|
||||
protected FilterImageLazyLoad $filter;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
$this->filter = new FilterImageLazyLoad([], 'filter_image_lazy_load', ['provider' => 'test']);
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::process
|
||||
*
|
||||
* @dataProvider providerHtml
|
||||
*
|
||||
* @param string $html
|
||||
* Input HTML.
|
||||
* @param string $expected
|
||||
* The expected output string.
|
||||
*/
|
||||
public function testProcess(string $html, string $expected): void {
|
||||
$this->assertSame($expected, $this->filter->process($html, 'en')->getProcessedText());
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides data for testProcess.
|
||||
*
|
||||
* @return array
|
||||
* An array of test data.
|
||||
*/
|
||||
public function providerHtml(): array {
|
||||
return [
|
||||
'lazy loading attribute already added' => [
|
||||
'input' => '<p><img src="foo.png" loading="lazy"></p>',
|
||||
'output' => '<p><img src="foo.png" loading="lazy" /></p>',
|
||||
],
|
||||
'eager loading attribute already added' => [
|
||||
'input' => '<p><img src="foo.png" loading="eager"/></p>',
|
||||
'output' => '<p><img src="foo.png" loading="eager" /></p>',
|
||||
],
|
||||
'image dimensions provided' => [
|
||||
'input' => '<p><img src="foo.png" width="200" height="200"/></p>',
|
||||
'<p><img src="foo.png" width="200" height="200" loading="lazy" /></p>',
|
||||
],
|
||||
'width image dimensions provided' => [
|
||||
'input' => '<p><img src="foo.png" width="200"/></p>',
|
||||
'<p><img src="foo.png" width="200" /></p>',
|
||||
],
|
||||
'height image dimensions provided' => [
|
||||
'input' => '<p><img src="foo.png" height="200"/></p>',
|
||||
'<p><img src="foo.png" height="200" /></p>',
|
||||
],
|
||||
'invalid loading attribute' => [
|
||||
'input' => '<p><img src="foo.png" width="200" height="200" loading="foo"></p>',
|
||||
'output' => '<p><img src="foo.png" width="200" height="200" loading="lazy" /></p>',
|
||||
],
|
||||
'no image tag' => [
|
||||
'input' => '<p>Lorem ipsum...</p>',
|
||||
'output' => '<p>Lorem ipsum...</p>',
|
||||
],
|
||||
'no image dimensions provided' => [
|
||||
'input' => '<p><img src="foo.png"></p>',
|
||||
'output' => '<p><img src="foo.png" /></p>',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
|
@ -16,7 +16,7 @@ filters:
|
|||
status: true
|
||||
weight: -10
|
||||
settings:
|
||||
allowed_html: '<a href hreflang> <em> <strong> <cite> <blockquote cite> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id> <p> <br> <img src alt height width data-entity-type data-entity-uuid data-align data-caption> <drupal-media data-entity-type data-entity-uuid data-view-mode data-align data-caption alt title>'
|
||||
allowed_html: '<a href hreflang> <em> <strong> <cite> <blockquote cite> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id> <p> <br> <img src alt loading height width data-entity-type data-entity-uuid data-align data-caption> <drupal-media data-entity-type data-entity-uuid data-view-mode data-align data-caption alt title>'
|
||||
filter_html_help: false
|
||||
filter_html_nofollow: false
|
||||
filter_align:
|
||||
|
@ -49,6 +49,12 @@ filters:
|
|||
status: true
|
||||
weight: 0
|
||||
settings: { }
|
||||
filter_image_lazy_load:
|
||||
id: filter_image_lazy_load
|
||||
provider: filter
|
||||
status: true
|
||||
weight: 15
|
||||
settings: { }
|
||||
media_embed:
|
||||
id: media_embed
|
||||
provider: media
|
||||
|
|
|
@ -28,6 +28,12 @@ filters:
|
|||
status: true
|
||||
weight: 10
|
||||
settings: { }
|
||||
filter_image_lazy_load:
|
||||
id: filter_image_lazy_load
|
||||
provider: filter
|
||||
status: true
|
||||
weight: 15
|
||||
settings: { }
|
||||
editor_file_reference:
|
||||
id: editor_file_reference
|
||||
provider: editor
|
||||
|
|
|
@ -36,6 +36,12 @@ filters:
|
|||
status: true
|
||||
weight: 9
|
||||
settings: { }
|
||||
filter_image_lazy_load:
|
||||
id: filter_image_lazy_load
|
||||
provider: filter
|
||||
status: true
|
||||
weight: 15
|
||||
settings: { }
|
||||
editor_file_reference:
|
||||
id: editor_file_reference
|
||||
provider: editor
|
||||
|
|
|
@ -27,6 +27,12 @@ filters:
|
|||
status: true
|
||||
weight: 10
|
||||
settings: { }
|
||||
filter_image_lazy_load:
|
||||
id: filter_image_lazy_load
|
||||
provider: filter
|
||||
status: true
|
||||
weight: 15
|
||||
settings: { }
|
||||
editor_file_reference:
|
||||
id: editor_file_reference
|
||||
provider: editor
|
||||
|
|
Loading…
Reference in New Issue