Skip to content
Snippets Groups Projects
Verified Commit a266ba6e authored by Alex Pott's avatar Alex Pott
Browse files

Issue #3247795 by heddn, Graber, ravi.shankar, yogeshmpawar, Anchal_gupta, Wim...

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 bd4448db
No related branches found
No related tags found
No related merge requests found
Showing
with 265 additions and 23 deletions
......@@ -543,7 +543,7 @@ public function testEditorFileReferenceIntegration() {
$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 @@ public function testEditorFileReferenceIntegration() {
$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 @@
* @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 @@ public function process($text, $langcode) {
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);
}
}
}
......
<?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 @@ public function testEditorFileReferenceFilter() {
$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 @@ public function testEditorFileReferenceFilter() {
[$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());
......
<?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>&lt;img loading=&quot;eager&quot;&gt;</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);
}
}
<?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
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment