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")
}
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
}
style
is an object that contains the list of attributes you want this extension to apply. In our case, we need to applyunderline
as thetextDecoration
property of theunderline
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 callingthis.trix.attributeIsActive
, trix will call theparser
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 caninherit
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()
}
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>
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
}
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>
This button is disabled by default. Because to apply underlineColor
, the selection needs to have underline
attribute as active.
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.
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.
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")
}
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]
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)
}
}
Finally, we end up with this.
Thank for reading this. Have a good day, and Happy Coding.
Top comments (2)
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?
Glad you liked it!.
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.
I found most of them through Google :)