Add font-size controls to Trix's toolbar
Tags: rails, javascript, ruby, trix
Published: 2022-04-08 23:12:53

This is a third blog-post on extending Trix with some new abilities. You can read the previous ones here


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

{% post https://dev.to/rockwell/adding-underline-underline-color-tool-to-trix-2485 %}

**Off we go**

In this part we will add another ability to our Trix's attributes. This time we will try adding a font-size controls, much like Google Docs does it. Onwards.

### Registering the extension 

As with all other extensions we added, we need to inform trix that to recognise the new attribute we are adding, and so, we modify `setupTrix` method, and add the following.

```js
setupTrix() {
 Trix.config.textAttributes.fontSize = {
   styleProperty: "font-size",
   inheritable: 1
 }

 this.trix = this.element.querySelector("trix-editor")
}
```

Now, because the font-size value is a dynamic value, i.e it's from user input, and the value isn't a constant, we use the form which is used for dynamic extensions. See [this](https://dev.to/rockwell/adding-underline-underline-color-tool-to-trix-2485) for more information.

### Adding Markup

Next up, we add some markup that makes the font-size controls visible to the user.

![Font size controls](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o14frio857em0a31004y.png)

### Stimulus Controller 

The logic for handling the font-size is a bit too much to place inside the Trix controller, it's already growing, and we should strive to put each tool into it's own controller.

```js
export default class extends Controller {
  static targets = ["input"]
  static values = {
    size: { type: Number, default: 14 }
  }

  onKeyPress(e) {
    if(e.key === "Enter") {
      e.preventDefault()
      this.submit()
    }
  }

  increase() {
    this.dispatch("change", {
      detail: new Font(++this.sizeValue),
    })
  }

  decrease() {
    this.dispatch("change", {
      detail: new Font(--this.sizeValue)
    })
  }

  // private

  submit() {
    this.sizeValue = this.inputTarget.value
    this.dispatch("change", {
      detail: new Font(this.sizeValue),
    })
  }

  sizeValueChanged() {
    this.inputTarget.value = this.sizeValue
  }
}
```

The class is simple, it has two public methods, `increase` and `decrease`, each whom will add one or deduct one from the value.

When the value changes we dispatch an event that the trix_controller listens to. The `Font` class is a simple object that allows the trix_controller to either get the value in `px` or `rem`.

```js
class Font {
  constructor(size) {
    this.size = size
  }

  get rem() {
    return `${this.size * 0.0625}rem`
  }

  get px() {
    return `${this.size}px`
  }
}
```

It uses JavaScript's [getters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get) to return the size in either px or rem, as the client wishes so. 

Now, let's listen for the event inside the `Trix` controller.

First up, we need to listen to the event,

```html
<div
data-action="color-picker:change->trix#changeColor font-size:change->trix#changeSelectionFontSize"
```
notice that on `font-size:change` event, we invoke `changeSelectionFontSize`

```js
changeSelectionFontSize({ detail: font }) {
  this.trixEditor.activateAttribute("fontSize", font.px)
}
```

Notice, we are simply getting the font-size in pixels by calling `Font#px` getter. 

{% embed https://youtu.be/fG5bukm9Iok %}

### Syncing

Now, it can happen that different parts of the content have different font-sizes. We need to update the font-size `<input>` with the font-size at the current cursor location.

This should also be very straightforward. We need to listen to each keypress on the editor. Then, determine if the current `Piece` has the `fontSize` attribute, if so, we notify `font_size` controller to sync it's state with cursor position. Translated into code, it looks like this, we add to the `TrixController#sync`, which is called on each keystroke.

```js
sync() {
  if (this.pieceAtCursor.attributes.has("fontSize")) {
    this.dispatch("font-size:sync", {
      target: this.fontSizeControlsTarget,
      detail: this.pieceAtCursor.getAttribute("fontSize")
    })
  }
}

get pieceAtCursor() {
  return this.trixEditorDocument.getPieceAtPosition(this.trixEditor.getPosition())
}

```

When we detect that the current piece has the `fontSize` attribute, we alert `FontSizeController` to sync it's internal state with the editor. The payload(detail) will be `font-size` of the piece, which we get by calling `Piece#getAttribute`.

Next, let's wire the `FontSizeController` to correctly update it's internal state.

```js
sync({ detail: fontSizeString }) {
  this.sizeValue = Font.rawNumberFrom(fontSizeString)
}

class Font {
  static rawNumberFrom(fontSizeString) {
    return Number.parseInt(fontSizeString)
  }
}
```

The payload will be a `font-size` string, i.e in the format of `number{type}`, so we call `Font.rawNumberFrom` static methods which gets the number in the string. We make use of JavaScript's built in `Number.parseInt` which correctly extracts the number from the string.

{% embed https://youtu.be/ES8OOIc2Tgs %}

### Via input

It can happen that the user enters a specific font-size into the input, and when they press `Enter` we `submit` by dispatching a `change` event.

Because the font-size input, when focused, will steal the active state from the trix editor, we need to remember the user's selection, so that next time we activate the font-size attribute, we activate it on the user's original selection.

First, let's listen for the focus event

```html
<input type="number"
  data-trix-target="fontSizeInput"
  data-font-size-target="input"
  data-action="focus->trix#markSelection keydown->font-size#onKeyPress"
```

Notice the `focus->trix#markSelection`

```js
  markSelection() {
    this.trixEditor.activateAttribute("frozen")
    this.fontSizeInputTarget.focus()
    this.trix.blur()
  }
```

Because calling `activateAttribute` will cause trix to focus, after activating the `frozen` attribute we `blur`.

> Gotcha: the `frozen` attribute ships by default in Trix. It's a simple attribute that allows to communicate visual feedback to the user and give the impression that the selected text is in fact still selected.

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

Notice that, when the font-size input is focused, the "Frozen" text is still highlighted, that is because we activated the `frozen` attribute.

To remove the `frozen` attribute after the font-size was applied on the selection, we simply tell trix to remove it.

```js
changeSelectionFontSize({ detail: font }) {
  this.trixEditor.activateAttribute("fontSize", font.px)
  this.trixEditor.deactivateAttribute("frozen")
}
```
If we don't remove the `frozen` attribute, we will end up with a weird UI state, see below gif.

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

After the font-size is applied and the user continues writing, the "Frozen" text still appears to be selected, however because we are explicitly telling Trix to remove the attribute. We end up with something far better, like this.

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

There are still a few more edge-cases to cover. But, that should be enough for you to add font-size controls to your Trix toolbars.

You can clone the repository [here](https://github.com/rockwellll/custom_trix_actions)

Hope you enjoyed this one. Good day, and Happy Coding!.

**Resources**

- JavaScript [getters ](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get)
- [Heroicons](https://heroicons.com/)