0

Is it possible to automatically add a class to each <ul> element added to the CKEditor WYSIWYG?

In my project, I decided to not style ul > li in order to not have to rewrite CSS for each ul Drupal can display (i.e. menus or contextual links). So, I have only one CSS rule for my unordered lists: ul.my_custom_class li. I used to add a custom style in CKEditor's Styles dropdown that add this class, but it's not a perfect solution, as some administrators forget to use it (or sometimes, CKEditor does not recognize ul ou a since Drupal 8.4.x, see this thread).

The perfect solution would be that, each time you add an unordered list into the WYSIWYG, instead of having ul li, you'd have ul.my_custom_class li. I know I can use an other CSS rules like .ckeditor ul to make it work, but I want to be able to use this specific class anywhere in the website.

I've found some methods in the CKEditor docs but I don't really know where to use them on Drupal 8.

  • Your best bet is finding a CKEditor plugin that already does what you need and integrate it to D8's CKEditor. If your use case is too special for this approach, you'll have to provide more details on what you like to achieve. For automatic assignment of index classes e.g., I'd suggest custom text filters instead. But all wild guessing without more context. – Mario Steinitz Dec 18 '17 at 12:54
  • Each time you click in the "Unordered list" button in CKEditor, a ul > li gets injected. What I want is ul.my_custom_class li. – crtlf Dec 18 '17 at 12:59
  • You could add custom classes to the Styles drop down (so anytime a UL is highlighted, you can apply the class). Or you could style ULs within the designated field output too. – Kevin Dec 18 '17 at 13:01
  • I don't want to use the Styles dropdown as I want my users to only use the ul.my_custom_class li and not ul > li. @Kevin, could you provide more details for your second approach ? – crtlf Dec 18 '17 at 13:03
  • 1
    Ok, understood your use case. My suggestion: Do not enforce the class in CKEditor (will be forgotten by editors anyway). Create a custom text filter that automatically adds the class to the markup when the text format is rendered. This way your content editors can just use unnumbered lists and you don't have to worry about forgotten classes. – Mario Steinitz Dec 18 '17 at 13:33
3

According to your description, you want to automatically add the class to all lists within the CKEditor content for better theming.

I suggest not doing so within the CKEditor. Use a custom text filter instead. This avoids users accidentially forgetting to add the class manually and keeps your database stored contents cleaner.

The custom filter could parse the content and automatically add the classes when the text format is rendered. A good guide on getting started with creating custom text filters is this one: https://www.lullabot.com/articles/creating-a-custom-filter-in-drupal-8

In your process method you should use a simple DOM parser to add classes to all ul tags (not tested though, just for illustration):

namespace Drupal\my_module\Plugin\Filter;

use Drupal\Component\Utility\Html;
use Drupal\filter\FilterProcessResult;
use Drupal\filter\Plugin\FilterBase;

/**
 * Drupal 8 filter to automatically add class to unnumbered lists.
 *
 * @Filter(
 *   id = "my_lists_filter",
 *   title = @Translation("My lists filter"),
 *   description = @Translation("Automatically adds my_class to unnumbered lists."),
 *   type = \Drupal\filter\Plugin\FilterInterface::TYPE_MARKUP_LANGUAGE,
 * )
 */
class MyListsFilter extends FilterBase {

  /**
   * {@inheritdoc}
   */
  public function process($text, $langcode) {
    // Create DOM document.
    $dom = Html::load($text);

    // Get all UL tags.
    $lists_query = '//ul';

    // Get DOM nodes from XPath query.
    $xpath = new \DOMXPath($dom);
    $nodes = $xpath->query($lists_query);
    // Whether we have results.
    if ($nodes->length > 0) {
      // Iterate over list elements and add our class.
      foreach ($nodes as $node) {
        $classes = explode(' ', $node->getAttribute('class'));
        $classes[] = 'my_class';
        $node->setAttribute('class', join(' ', array_unique($classes)));
      }
    }

    return new FilterProcessResult(Html::serialize($dom));
  }

}
  • This is clever. – Kevin Dec 18 '17 at 14:28
  • 1
    Just for illustration. Personally, I'd use plain CSS in my theme and omit manipulating the DOM for the beforementioned task. But if he really wants to have the classes added, the filter should do. – Mario Steinitz Dec 18 '17 at 14:44
  • Works perfectly, thank you ! I just had to modify the XPath query to make it work, i edited the answer. – crtlf Dec 18 '17 at 16:00
  • Glad it helped. Your edit doesn't seem to appear. Could you share it within the comments? - I'll happily add it to the answer for future users coming by with a similar question. – Mario Steinitz Dec 19 '17 at 1:14
  • @MarioSteinitz I deleted $nodes = new \DOMXPath($dom)->query($lists_query); and added $query = new \DOMXPath($dom); $nodes = $query->query($lists_query); instead. I still don't know why, but the query function does not seems to work when in one line. – crtlf Dec 19 '17 at 9:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.