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")
}
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.
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
}
}
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`
}
}
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"
notice that on font-size:change
event, we invoke changeSelectionFontSize
changeSelectionFontSize({ detail: font }) {
this.trixEditor.activateAttribute("fontSize", font.px)
}
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())
}
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)
}
}
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"
Notice the focus->trix#markSelection
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.
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")
}
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
Top comments (0)