DEV Community

Ahmad khattab
Ahmad khattab

Posted on

Add font-size controls to Trix's toolbar

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

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.

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

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

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 for more information.

Adding Markup

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

Font size controls

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.

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

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

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

  get px() {
    return `${this.size}px`
  }
}
Enter fullscreen mode Exit fullscreen mode

It uses JavaScript's getters 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,

<div
data-action="color-picker:change->trix#changeColor font-size:change->trix#changeSelectionFontSize"
Enter fullscreen mode Exit fullscreen mode

notice that on font-size:change event, we invoke changeSelectionFontSize

changeSelectionFontSize({ detail: font }) {
  this.trixEditor.activateAttribute("fontSize", font.px)
}
Enter fullscreen mode Exit fullscreen mode

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

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.

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())
}

Enter fullscreen mode Exit fullscreen mode

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.

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

class Font {
  static rawNumberFrom(fontSizeString) {
    return Number.parseInt(fontSizeString)
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

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

<input type="number"
  data-trix-target="fontSizeInput"
  data-font-size-target="input"
  data-action="focus->trix#markSelection keydown->font-size#onKeyPress"
Enter fullscreen mode Exit fullscreen mode

Notice the focus->trix#markSelection

  markSelection() {
    this.trixEditor.activateAttribute("frozen")
    this.fontSizeInputTarget.focus()
    this.trix.blur()
  }
Enter fullscreen mode Exit fullscreen mode

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.

.

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.

changeSelectionFontSize({ detail: font }) {
  this.trixEditor.activateAttribute("fontSize", font.px)
  this.trixEditor.deactivateAttribute("frozen")
}
Enter fullscreen mode Exit fullscreen mode

If we don't remove the frozen attribute, we will end up with a weird UI state, see below 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.

.

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

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

Resources

Discussion (0)