DEV Community

Ahmad khattab
Ahmad khattab

Posted on

Adding Underline, Underline color tools to Trix

In the previous post. We managed to add a couple of new attributes to Trix's toolbar, text-color and background-color, respectively.

Here is the link to the first part, where we added color and background color tools to Trix.

Before we continue, here is the list of terminologies to be aware of.

Terminologies

  • Editor: The Trix editor.
  • Document: The Trix document.
  • Piece: A substring of a text in the Document(see above)
  • Attribute: a transformation to apply on the selection
  • Activate attribute: apply attribute
  • Deactivate attribute: remove attribute

In this post, we will add another attribute, which is underline.

First of all, we need to add the extension to Trix. We modify the setupTrix method and add this bit of code.

setupTrix() {

   Trix.config.textAttributes.foregroundColor = {
      styleProperty: "color",
      inheritable: 1
    }

    Trix.config.textAttributes.backgroundColor = {
      styleProperty: "background-color",
      inheritable: 1
    }

    // new attribute
    Trix.config.textAttributes.underline = {
      style: { textDecoration: "underline" },
      parser: function(element) {
        return element.style.textDecoration === "underline"
      },
      inheritable: 1
    }

    this.trix = this.element.querySelector("trix-editor")
  }
Enter fullscreen mode Exit fullscreen mode

Notice, there is a slight difference between the way we add the underline color and both textColor and backgroundColor.

The first difference is that in the underline extension, there is the style object. But, in the previous two attributes there was only styleProperty. Because both textColor and backgroundColor were being set dynamically, i.e a random color through the color picker. We need to extend it like so, the extension object should have the styleProperty which corresponds to a valid CSS property(kebab-case and not camelCase).

However, because the underline tool is static, i.e it's only one property being applied to the selection, then we can use the latter form.

    Trix.config.textAttributes.underline = {
      style: { textDecoration: "underline" },
      parser: function(element) {
        return element.style.textDecoration === "underline"
      },
      inheritable: 1
    }
Enter fullscreen mode Exit fullscreen mode
  • style is an object that contains the list of attributes you want this extension to apply. In our case, we need to apply underline as the textDecoration property of the underline extension. Notice here we use the camelCase version instead of the kebab-case we used in the previous two.

  • parser is an optional function that returns a boolean indicating if this attribute is applied or not. When calling this.trix.attributeIsActive, trix will call the parser methods of the attributes. If you see yourself not needing to determine if this attribute is active or not, you can omit this callback.

  • inheritable determines if child nodes of this element who have other attributes applied on them can inherit this attribute or not.

Next, let's add a method that toggles the underline attribute we just added.

  toggleUnderline() {
    if(this.trixEditor.attributeIsActive("underline")) {
      this.trixEditor.deactivateAttribute("underline")
    } else {
      this.trixEditor.activateAttribute("underline")
    }

    this.trix.focus()
  }
Enter fullscreen mode Exit fullscreen mode

We first determine if the attribute is active by calling attributeIsActive method, if so we toggle it off, and vice-versa. Unfortunately, Trix does not provide a toggleAttribute method. At the end, we give focus back to the editor.

Finally, let's add a button to allow for such behaviour, inside home/index.html.erb

      <button data-action="click->trix#toggleUnderline" class="w-5 h-5 flex items-center justify-center">
        <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 360 360" style="enable-background:new 0 0 360 360;" xml:space="preserve">
          <g>
            <rect x="62.877" y="330" width="234.245" height="30"/>
            <path d="M180,290c61.825,0,112.123-50.298,112.123-112.122V0h-30v177.878C262.123,223.16,225.283,260,180,260   s-82.123-36.84-82.123-82.122V0h-30v177.878C67.877,239.702,118.175,290,180,290z"/>
          </g>
        </svg>
      </button>

Enter fullscreen mode Exit fullscreen mode

And, voila. It looks like this

.

Underline Color

It might make sense to add another tool aside from underline. Which is underline color. Adding it is also relatively straightforward, let's add another extension to setupTrix method.

    Trix.config.textAttributes.underlineColor = {
      styleProperty: "text-decoration-color",
      inheritable: 1
    }
Enter fullscreen mode Exit fullscreen mode

Remember, because this attribute is a dynamic attribute, meaning each color is a random value, a value which will be determined by the user. We use the same object as we used for textColor and backgroundColor.

Next, let's add the icon to the UI.

  <button disabled data-controller="color-picker dropdown" data-trix-target="underlineColorPicker"   data-action="click->dropdown#toggle" class="relative text-gray-300">
        <span class="w-5 h-5 flex items-center justify-center">
          <svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 360 360" style="enable-background:new 0 0 360 360;" xml:space="preserve">
            <g>
              <rect x="62.877" y="330" width="234.245" height="30"/>
              <path d="M180,290c61.825,0,112.123-50.298,112.123-112.122V0h-30v177.878C262.123,223.16,225.283,260,180,260   s-82.123-36.84-82.123-82.122V0h-30v177.878C67.877,239.702,118.175,290,180,290z"/>
            </g>
          </svg>
        </span>

        <span data-dropdown-target="menu" data-action="click@window->dropdown#hide" class="hidden absolute -right-[20rem] bg-indigo-700 rounded-md p-2 shadow-xl">
          <span data-color-picker-target="picker">

          </span>
        </span>
      </button>
Enter fullscreen mode Exit fullscreen mode

This button is disabled by default. Because to apply underlineColor, the selection needs to have underline attribute as active.

Underline color icon is disabled

Notice, the second underline icon is disabled.

We need to listen to the keypress and determine if the current cursor location has the underline attribute as active, first let's hook up some event listeners to <trix-editor> element.

    <%= rich_text_area_tag "content", "", class: "w-full no-toolbar", data: { action: "keydown->trix#sync keyup->trix#sync" } %>

Enter fullscreen mode Exit fullscreen mode

Next, let's add the sync method.

  sync() {
    if(this.trixEditor.attributeIsActive("underline")) {
      this.underlineColorPickerTarget.disabled = false
      this.underlineColorPickerTarget.classList.remove("text-gray-300")
    } else {
      this.underlineColorPickerTarget.disabled = true
      this.underlineColorPickerTarget.classList.add("text-gray-300")
    }
  }
Enter fullscreen mode Exit fullscreen mode

It simply checks if underline is active, if so, it enables the second underline icon, else it will disable it again.

.

Notice, when the cursor enters the text that is underlined, it enables the underline color icon. But, if the text does not have the underline attribute active. It disables the underline color icon again.

Now, we need to select the text in the cursor location when the user opens the underline color picker. Because it's a button, when clicked, it will steal the focus from the editor.

What we need to do is, when the color picker modal is opened

  • Get the piece at the current cursor location.
  • Determine if the piece has underline attribute active.
    • if active, set the selection as the current piece.
  toggleUnderlineColorPicker() {
    const piece = this.trixEditorDocument.getPieceAtPosition(this.trixEditor.getPosition());

    if (piece.attributes.has("underline")) {
      const indexOfPiece = this.trixEditorDocument.toString().indexOf(piece.toString())
      const textRange = [indexOfPiece, indexOfPiece + piece.length]
      this.trixEditor.setSelectedRange(textRange)
    }

    this.underlineColorPickerModalTarget.classList.toggle("hidden")
  }
Enter fullscreen mode Exit fullscreen mode

Piece: A substring of text in the Document.

First, we get the current piece(substring) at the cursor location. A piece is simply a Piece class that encapsulates some logic and makes our lives easier.

After getting the piece(an instance of Piece class) we check if it has the underline attribute, thanks to Trix's well named methods, it reads naturally as piece.has(attributeName).

If the piece has the underline attribute. We get the index of the piece, we get the string representation of the document through this.trixEditorDocument.toString(). After that, we construct a range(an array of two elements). Typically, a range in the context of using Trix is an array of two elements

const range = [start_index, end_index]
Enter fullscreen mode Exit fullscreen mode

After constructing the textRange range, we inform trix to mark the range as active by calling this.trixEditor.setSelectedRange(textRange). Eventually, we will toggle the color picker modal.

Because we already have two other color pickers, we need to determine the source of the color change, it's simply another condition to check for

  changeColor(e) {
    this.colorValue = e.detail
    if(this.backgroundColorTarget.contains(e.target)) {
      this.trixEditor.activateAttribute("backgroundColor", e.detail)
    } else if(this.textColorTarget.contains(e.target)) {
      this.trixEditor.activateAttribute("foregroundColor", e.detail)
    } else {
      this.trixEditor.activateAttribute("underlineColor", e.detail)
    }
  }
Enter fullscreen mode Exit fullscreen mode

Finally, we end up with this.

.

Thank for reading this. Have a good day, and Happy Coding.

Resources

Discussion (2)

Collapse
gumatias profile image
Gustavo Matias

Thanks for sharing this. Been curious lately how to extend Basecamp’s editor. Very helpful.

Do you think Trix could or would want to eventually turn into of more powerful WYSIWYG editor like CKEditor 5 for example?

Recently worked on a big project that required those types of collaborative features like Google docs and sort of wished I could go with Trix instead of CKEditor.

Btw, How did you come up with the svg buttons?

Collapse
rockwell profile image
Ahmad khattab Author

Thanks for sharing this. Been curious lately how to extend Basecamp’s editor. Very helpful.

Glad you liked it!.

Do you think Trix could or would want to eventually turn into of more powerful WYSIWYG editor like CKEditor 5 for example?

As much as i want it to happen. That's not likely. Trix was mainly built and maintained by javan and sam. Both whom left Basecamp last year due to the Basecamp drama.

That's a bummber because Trix is very powerful. And the lack of documentation makes a-lot of people shy away from it, myself included. But, fortunately i was able to "wrap" my head around how to work with Trix.

Btw, How did you come up with the svg buttons?

I found most of them through Google :)