reverted: --- b/core/modules/filter/filter.post_update.php +++ /dev/null @@ -1,39 +0,0 @@ -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' => 'filter', - 'status' => TRUE, - // Place lazy loading after editor file reference. - 'weight' => $configuration['editor_file_reference']['weight'] + 1, - 'settings' => [], - ]; - $collection->setConfiguration($configuration); - $format->save(); - } - } - } -} reverted: --- b/core/modules/filter/src/Plugin/Filter/FilterImageLazyLoad.php +++ /dev/null @@ -1,124 +0,0 @@ -<img loading="eager">."), - * type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE, - * weight = 15 - * ) - */ -final class FilterImageLazyLoad extends FilterBase implements ContainerFactoryPluginInterface { - - /** - * The entity repository. - * - * @var \Drupal\Core\Entity\EntityRepositoryInterface - */ - protected $entityRepository; - - /** - * The image factory. - * - * @var \Drupal\Core\Image\ImageFactory - */ - protected $imageFactory; - - /** - * Constructs a \Drupal\filter\Plugin\Filter\FilterImageLazyLoad object. - * - * @param array $configuration - * A configuration array containing information about the plugin instance. - * @param string $plugin_id - * The plugin_id for the plugin instance. - * @param mixed $plugin_definition - * The plugin implementation definition. - * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository - * The entity repository. - * @param \Drupal\Core\Image\ImageFactory $image_factory - * The image factory. - */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityRepositoryInterface $entity_repository, ImageFactory $image_factory) { - $this->entityRepository = $entity_repository; - $this->imageFactory = $image_factory; - parent::__construct($configuration, $plugin_id, $plugin_definition); - } - - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self { - return new self( - $configuration, - $plugin_id, - $plugin_definition, - $container->get('entity.repository'), - $container->get('image.factory') - ); - - } - - /** - * {@inheritdoc} - */ - public function process($text, $langcode): FilterProcessResult { - $result = new FilterProcessResult($text); - - // If there are no images, exit early. - if (stripos($text, 'setProcessedText($this->transformImages($text)); - } - - /** - * Transform markup of images to include loading="lazy". - * - * @param string $text - * The markup to transform. - * - * @return string - */ - 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. - foreach ($xpath->query('//img[not(@loading) and @data-entity-type="file" and @data-entity-uuid]') as $element) { - assert($element instanceof \DOMElement); - $uuid = $element->getAttribute('data-entity-uuid'); - $file = $this->entityRepository->loadEntityByUuid('file', $uuid); - if ($file instanceof FileInterface) { - $image = $this->imageFactory->get($file->getFileUri()); - $width = $image->getWidth(); - $height = $image->getHeight(); - // Set dimensions to avoid content layout shift (CLS). - if ($width !== NULL && !$element->hasAttribute('width')) { - $element->setAttribute('width', (string) $width); - } - if ($height !== NULL && !$element->hasAttribute('height')) { - $element->setAttribute('height', (string) $height); - } - // If dimensions are specified, then set lazy loading. - if ($element->hasAttribute('width') && $element->hasAttribute('height')) { - $element->setAttribute('loading', 'lazy'); - } - } - } - return Html::serialize($dom); - } - -} reverted: --- b/core/modules/filter/tests/src/Functional/Update/FilterConfigTest.php +++ /dev/null @@ -1,48 +0,0 @@ -databaseDumpFiles = [ - __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.8.0.bare.standard.php.gz', - ]; - } - - /** - * Tests upgrading filter settings. - * - * @see \filter_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')); - - // Run updates. - $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']); - } - -} reverted: --- b/core/modules/filter/tests/src/Unit/FilterImageLazyLoadTest.php +++ /dev/null @@ -1,72 +0,0 @@ -prophesize(FileInterface::class); - $file->getFileUri()->willReturn('foo.png'); - $entity_repository = $this->prophesize(EntityRepositoryInterface::class); - $entity_repository->loadEntityByUuid('file', 'a6d88b01-3b5e-4c02-bf26-24a0c48d61cd')->willReturn($file->reveal()); - $image = $this->prophesize(Image::class); - $image->getHeight()->willReturn(100); - $image->getWidth()->willReturn(100); - $image_factory = $this->prophesize(ImageFactory::class); - $image_factory->get('foo.png')->willReturn($image->reveal()); - $this->filter = new FilterImageLazyLoad([], 'filter_image_lazy_load', ['provider' => 'test'], $entity_repository->reveal(), $image_factory->reveal()); - 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' => ['

', '

'], - 'eager loading attribute already added' => ['

', '

'], - 'no image tag' => ['

Lorem ipsum...

', '

Lorem ipsum...

'], - 'no loading attribute nor uuid' => ['

', '

'], - 'no loading attribute but uuid' => ['

', '

'], - ]; - } - -} diff -u b/core/profiles/demo_umami/config/install/filter.format.basic_html.yml b/core/profiles/demo_umami/config/install/filter.format.basic_html.yml --- b/core/profiles/demo_umami/config/install/filter.format.basic_html.yml +++ b/core/profiles/demo_umami/config/install/filter.format.basic_html.yml @@ -49,8 +49,8 @@ status: true weight: 0 settings: { } - filter_image_lazy_load: - id: filter_image_lazy_load + editor_image_lazy_load: + id: editor_image_lazy_load provider: filter status: true weight: 10 diff -u b/core/profiles/standard/config/install/filter.format.basic_html.yml b/core/profiles/standard/config/install/filter.format.basic_html.yml --- b/core/profiles/standard/config/install/filter.format.basic_html.yml +++ b/core/profiles/standard/config/install/filter.format.basic_html.yml @@ -36,8 +36,8 @@ status: true weight: 9 settings: { } - filter_image_lazy_load: - id: filter_image_lazy_load + editor_image_lazy_load: + id: editor_image_lazy_load provider: filter status: true weight: 15 diff -u b/core/profiles/standard/config/install/filter.format.full_html.yml b/core/profiles/standard/config/install/filter.format.full_html.yml --- b/core/profiles/standard/config/install/filter.format.full_html.yml +++ b/core/profiles/standard/config/install/filter.format.full_html.yml @@ -27,8 +27,8 @@ status: true weight: 10 settings: { } - filter_image_lazy_load: - id: filter_image_lazy_load + editor_image_lazy_load: + id: editor_image_lazy_load provider: filter status: true weight: 15 only in patch2: unchanged: --- a/core/modules/editor/editor.module +++ b/core/modules/editor/editor.module @@ -182,6 +182,64 @@ function editor_form_filter_format_form_alter(&$form, FormStateInterface $form_s $form['actions']['submit']['#submit'][] = 'editor_form_filter_admin_format_submit'; } +/** + * Implements hook_form_FORM_ID_alter(). + */ +function editor_form_filter_format_edit_form_alter(array &$form, FormStateInterface $form_state, $form_id) { + // Add an additional validate callback so we can ensure the order of filters + // is correct. + $form['#validate'][] = 'editor_filter_format_edit_form_validate'; +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function editor_form_filter_format_add_form_alter(array &$form, FormStateInterface $form_state, $form_id) { + // Add an additional validate callback so we can ensure the order of filters + // is correct. + $form['#validate'][] = 'editor_filter_format_edit_form_validate'; +} + +/** + * Validate callback to ensure filter order and allowed_html are compatible. + */ +function editor_filter_format_edit_form_validate($form, FormStateInterface $form_state) { + if ($form_state->getTriggeringElement()['#name'] !== 'op') { + return; + } + + $lazy_load_enabled = $form_state->getValue([ + 'filters', + 'editor_image_lazy_load', + 'status', + ]); + + if ($lazy_load_enabled !== 1) { + return; + } + + $get_filter_label = function ($filter_plugin_id) use ($form) { + return (string) $form['filters']['order'][$filter_plugin_id]['filter']['#markup']; + }; + + $filters = $form_state->getValue('filters'); + + // The "editor_image_lazy_load" filter must run after "editor_file_reference". + $file_reference = $filters['editor_file_reference']; + $lazy_load = $filters['editor_image_lazy_load']; + if ($file_reference['weight'] >= $lazy_load['weight'] || $file_reference['status'] === 0) { + $error_message = new TranslatableMarkup( + 'The %filter must be enabled and placed before the %lazy-load-filter-label filter.', + [ + '%lazy-load-filter-label' => $get_filter_label('editor_image_lazy_load'), + '%filter' => $get_filter_label('editor_file_reference'), + ] + ); + + $form_state->setErrorByName('filters', $error_message); + } +} + /** * Button submit handler for filter_format_form()'s 'editor_configure' button. */ only in patch2: unchanged: --- a/core/modules/editor/editor.post_update.php +++ b/core/modules/editor/editor.post_update.php @@ -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('editor_image_lazy_load'); + $configuration['editor_image_lazy_load'] = [ + 'id' => 'editor_image_lazy_load', + 'provider' => 'filter', + 'status' => TRUE, + // Place lazy loading after editor file reference. + 'weight' => $configuration['editor_file_reference']['weight'] + 1, + 'settings' => [], + ]; + $collection->setConfiguration($configuration); + $format->save(); + } + } + } +} only in patch2: unchanged: --- /dev/null +++ b/core/modules/editor/src/Plugin/Filter/EditorImageLazyLoad.php @@ -0,0 +1,127 @@ +<img loading="eager">."), + * type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE, + * weight = 15 + * ) + */ +final class EditorImageLazyLoad extends FilterBase implements ContainerFactoryPluginInterface { + + /** + * The entity repository. + * + * @var \Drupal\Core\Entity\EntityRepositoryInterface + */ + protected $entityRepository; + + /** + * The image factory. + * + * @var \Drupal\Core\Image\ImageFactory + */ + protected $imageFactory; + + /** + * Constructs a FilterImageLazyLoad object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin ID for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository + * The entity repository. + * @param \Drupal\Core\Image\ImageFactory $image_factory + * The image factory. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityRepositoryInterface $entity_repository, ImageFactory $image_factory) { + $this->entityRepository = $entity_repository; + $this->imageFactory = $image_factory; + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self { + return new self( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity.repository'), + $container->get('image.factory') + ); + + } + + /** + * {@inheritdoc} + */ + public function process($text, $langcode): FilterProcessResult { + $result = new FilterProcessResult($text); + + // If there are no images, exit early. + if (stripos($text, 'setProcessedText($this->transformImages($text)); + } + + /** + * Transform markup of images to include loading="lazy". + * + * @param string $text + * The markup to transform. + * + * @return string + */ + 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. + foreach ($xpath->query('//img[not(@loading) and @data-entity-type="file" and @data-entity-uuid]') as $element) { + assert($element instanceof \DOMElement); + $uuid = $element->getAttribute('data-entity-uuid'); + $file = $this->entityRepository->loadEntityByUuid('file', $uuid); + if ($file instanceof FileInterface) { + $image = $this->imageFactory->get($file->getFileUri()); + $width = $image->getWidth(); + $height = $image->getHeight(); + // Set dimensions to avoid content layout shift (CLS). + if ($width !== NULL && !$element->hasAttribute('width')) { + $element->setAttribute('width', (string) $width); + } + if ($height !== NULL && !$element->hasAttribute('height')) { + $element->setAttribute('height', (string) $height); + } + // If dimensions are specified, then set lazy loading. + if ($element->hasAttribute('width') && $element->hasAttribute('height')) { + $element->setAttribute('loading', 'lazy'); + } + } + } + return Html::serialize($dom); + } + +} only in patch2: unchanged: --- /dev/null +++ b/core/modules/editor/tests/src/Functional/Update/EditorFilterConfigTest.php @@ -0,0 +1,47 @@ +databaseDumpFiles = [ + __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.8.0.bare.standard.php.gz', + ]; + } + + /** + * Tests upgrading filter settings. + * + * @see filter_post_update_image_lazy_load() + */ + public function testUpdateLazyImageLoad(): void { + $config = $this->config('filter.format.full_html'); + $this->assertArrayNotHasKey('editor_image_lazy_load', $config->get('filters')); + + $this->runUpdates(); + + $config = $this->config('filter.format.full_html'); + $filters = $config->get('filters'); + $this->assertArrayHasKey('editor_image_lazy_load', $filters); + $this->assertEquals($filters['editor_file_reference']['weight'] + 1, $filters['editor_image_lazy_load']['weight']); + } + +} only in patch2: unchanged: --- /dev/null +++ b/core/modules/editor/tests/src/Unit/EditorImageLazyLoadTest.php @@ -0,0 +1,72 @@ +prophesize(FileInterface::class); + $file->getFileUri()->willReturn('foo.png'); + $entity_repository = $this->prophesize(EntityRepositoryInterface::class); + $entity_repository->loadEntityByUuid('file', 'a6d88b01-3b5e-4c02-bf26-24a0c48d61cd')->willReturn($file->reveal()); + $image = $this->prophesize(Image::class); + $image->getHeight()->willReturn(100); + $image->getWidth()->willReturn(100); + $image_factory = $this->prophesize(ImageFactory::class); + $image_factory->get('foo.png')->willReturn($image->reveal()); + $this->filter = new EditorImageLazyLoad([], 'editor_image_lazy_load', ['provider' => 'test'], $entity_repository->reveal(), $image_factory->reveal()); + 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' => ['

', '

'], + 'eager loading attribute already added' => ['

', '

'], + 'no image tag' => ['

Lorem ipsum...

', '

Lorem ipsum...

'], + 'no loading attribute nor uuid' => ['

', '

'], + 'no loading attribute but uuid' => ['

', '

'], + ]; + } + +}