Extend drupalMedia CKEditor5 plugin with additional attributes

As of Drupal 10, CKEditor 5 is the default WYSIWYG editor and Media Library has been neatly adapted to it as it got a dedicated balloon toolbar, allowing users to quickly adjust embedded medias.

Default drupalMedia Toolbar

Attention was moved from dialogs to balloons which is faster and more accessible for better editing experience.

Now, what if we want to add addition configurations to control our embedded medias?       

Prior to CKEditor 5, the media embedding options were handled with AJAX, loading a Drupal form which allowed the use of usual Drupal API to add any additional configurations as described in Customizing the Edit Embedded Media form which is not the case anymore.

To create a more sophisticated interaction we need to create CKEditor plugins which is described in CKEditor 5 module and a good starting point for it is to check the starter template provided in CKEditor 5 Dev Tools.

However, there is an easy way to add additional items in toolbar via YAML definition which will add defined attributes to the drupalMedia model and based on those attribute values we can implement desired logic.

Let's go through the process by trying to achieve a real life useful config. Specifically, by default(at least with Drupal 10.0.0) media items have the attribute loading="lazy". This is great of course for end-user performance however, if the embedded image is placed in the initial load viewport of a webpage, this is not too good for Largest Contentful Paint metric.

Reported issue for LCP metric
PageSpeed Insights reporting issue with lazily loaded image

Let's fix this by adding an option to choose weather we want the loading attribute to be either lazy or eager.

Assuming we have a module with name: extendmedia

Create a file in the module's root: extendmedia.ckeditor5.yml with the following content:

  provider: extendmedia
      - drupalMedia.DrupalElementStyle
          - name: 'lazy'
            title: 'Lazy'
            icon: '<svg fill="#424242" width="64px" height="64px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_iconCarrier"><path d="M925.17 575.167c0-225.225-184.144-407.869-411.382-407.869-227.247 0-411.392 182.643-411.392 407.869s184.145 407.869 411.392 407.869c227.238 0 411.382-182.645 411.382-407.869zm40.96 0c0 247.922-202.557 448.829-452.342 448.829-249.794 0-452.352-200.906-452.352-448.829s202.558-448.829 452.352-448.829c249.785 0 452.342 200.908 452.342 448.829zM353.528 40.96H674.04c11.311 0 20.48-9.169 20.48-20.48S685.351 0 674.04 0H353.528c-11.311 0-20.48 9.169-20.48 20.48s9.169 20.48 20.48 20.48z"></path><path d="M493.305 20.48v129.577c0 11.311 9.169 20.48 20.48 20.48s20.48-9.169 20.48-20.48V20.48c0-11.311-9.169-20.48-20.48-20.48s-20.48 9.169-20.48 20.48z"></path><path d="M493.305 140.104v76.268c0 11.311 9.169 20.48 20.48 20.48s20.48-9.169 20.48-20.48v-76.268c0-11.311-9.169-20.48-20.48-20.48s-20.48 9.169-20.48 20.48zm-5.12 777.603v76.268c0 14.138 11.462 25.6 25.6 25.6s25.6-11.462 25.6-25.6v-76.268c0-14.138-11.462-25.6-25.6-25.6s-25.6 11.462-25.6 25.6zM95.312 595.647h76.902c11.311 0 20.48-9.169 20.48-20.48s-9.169-20.48-20.48-20.48H95.312c-11.311 0-20.48 9.169-20.48 20.48s9.169 20.48 20.48 20.48zm770.676 0h76.902c11.311 0 20.48-9.169 20.48-20.48s-9.169-20.48-20.48-20.48h-76.902c-11.311 0-20.48 9.169-20.48 20.48s9.169 20.48 20.48 20.48zm-338.212-44.468L358.755 383.53c-8.03-7.965-20.998-7.912-28.963.118s-7.912 20.998.118 28.963L498.931 580.26c8.03 7.965 20.998 7.912 28.963-.118s7.912-20.998-.118-28.963z"></path></g></svg>'
            attributeName: 'data-loading'
            attributeValue: 'lazy'
            modelElements: [ 'drupalMedia' ]
          - name: 'eager'
            title: 'Eager(good for when media is in initial viewport)'
            icon: '<svg version="1.1" id="Layer_3" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 4.83 50 49.68" enable-background="new 0 4.83 50 49.68" xml:space="preserve" fill="#424242"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_iconCarrier"> <rect x="1.438" y="6.232" fill="none" stroke="#424242" stroke-linecap="round" stroke-linejoin="round" width="47.125" height="46.875"></rect> <polyline fill="none" stroke="#424242" stroke-linecap="round" stroke-linejoin="round" points="1.755,44.13 14.375,31.875 23.5,38.625 37.25,23.125 48.25,31.625 "></polyline> <path fill="none" stroke="#445578" stroke-linecap="round" stroke-linejoin="round" d="M42.169,24.085 c0.364-0.791,0.567-1.67,0.567-2.598c0-3.444-2.792-6.236-6.236-6.236s-6.236,2.792-6.236,6.236c0,1.51,0.537,2.895,1.431,3.975"></path> </g></svg>'
            attributeName: 'data-loading'
            attributeValue: 'eager'
            modelElements: [ 'drupalMedia' ]
          - name: 'drupalMedia:loading'
            display: 'splitButton'
              - 'drupalElementStyle:loading:lazy'
              - 'drupalElementStyle:loading:eager'
            defaultItem: 'drupalElementStyle:loading:lazy'
    label: Extend Media
      - <drupal-media data-loading>
      plugins: [media_media]

In drupalElementStyles we defined the options we want while in drupalMedia we're have defining the toolbar item itself and gave it the name of loading with default value of drupalElementStyle:loading:lazy which is the one already added by default in the rendered image tag. 

Check out drupalelementstyleediting.js for option groups and drupalelementstyleui.js for display configuration examples.

Assuming the module is enabled we should get the new toolbar item available.

loading toolbar item in drupalMedia

However, this won't override the loading attribute in the image itself as our data-loading attribute is part of the media template and not the image itself.      

To address this, we can use hook_preprocess_media() to set our value as per configuration. Here is the code to do so.

 * Implements hook_preprocess_media().
 * Overrides loading attribute of a media image.
function extendmedia_preprocess_media(array &$variables): void {
  $dataAttribute = 'data-loading';
  $attributeVal = $variables['attributes'][$dataAttribute] ?? FALSE;
  $elements = Element::children($variables['elements']) ?? FALSE;
  $elements && $attributeVal && array_walk($elements, function ($elKey) use (&$variables, $attributeVal) {
    $contentEl = $variables['content'][$elKey] ?? FALSE;
    $contentElChildren = FALSE;
    $contentEl && $contentElChildren = Element::children($contentEl);
    $contentElChildren && array_walk($contentElChildren, function ($contentElKey) use (&$variables, $attributeVal, $elKey) {
      $variables['content'][$elKey][$contentElKey]['#item_attributes']['loading'] = $attributeVal;

As a result, we now have loading="eager".

Eager loading value code inspector screenshot


Check out all the code described above as a module at hugronaphor/extendmedia   

P.S: I found https://www.svgrepo.com being very helpful to generate SVG icons.   

Kudos to @wim-leers who pointed me to this + @hj and @lauriii for the work on drupalelementstyle plugin.

********************************** ************************* ************************ **************** ****************** *********** ************** ************* ************ *************