Adding Underline, Underline color tools to Trix
Tags: rails, javascript, ruby
Published: 2022-04-03 09:20:06

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.

{% post https://dev.to/rockwell/adding-custom-attributes-to-trixs-toolbar-21pl %}

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.

```js
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")
  }
```

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.

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

- `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.

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

    this.trix.focus()
  }
```

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`

```html
      <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>

```

And, voila. It looks like this   

<img width="100%" style="width:100%" src="https://media.giphy.com/media/Kw8HK4iwOlqCU278Pr/giphy.gif">.


### 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.

```js
    Trix.config.textAttributes.underlineColor = {
      styleProperty: "text-decoration-color",
      inheritable: 1
    }
```

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.

```html
  <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>
```

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



![Underline color icon is disabled](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vvcuq9u4roj6dcqlbsd5.png)

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" } %>

```

Next, let's add the `sync` method.

```js
  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")
    }
  }
```

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

<img width="100%" style="width:100%" src="https://media.giphy.com/media/qeCP8lFVGQX8An8eiv/giphy.gif">.

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.

```js
  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")
  }
```

> 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

```js
const range = [start_index, end_index]
```

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

```js
  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)
    }
  }
```

Finally, we end up with this.

<img src="https://media.giphy.com/media/Cxbm61prxEWZ7MxpZP/giphy.gif">.

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


### Resources 

- [The GitHub Repo](https://github.com/rockwellll/custom_trix_actions)
- [Trix's repo](https://github.com/basecamp/trix)