Trix Editor anpassen
Created: 05.05.2025, Updated: 05.06.2026,09:42:15Unterseiten Menü
Trix Editor für Ruby on Rails
Der Trix Editor ist ein Open-Source-Projekt von 37signals, den Entwicklern von Ruby on Rails. Dieser wurde entwickelt, um das bestmögliche Bearbeitungserlebnis zu bieten, wird z.B. verwendet auf Basecamp und HEY. Der Trix-editor (WYSIWYG) ist der standard Editor für Ruby on Rails mit der ActionText Erweiterung.
Trix Installation
Um die Trix Editor funktionalität zu erhalten, muss zuerst ActionText installiert werden.
ActionText wird mit folgendem Kommando installiert:
ActionText wird mit folgendem Kommando installiert:
$ bin/rails action_text:install
Es führt Folgendes aus:
- Installiert die JavaScript-Pakete für trix und @rails/actiontext und fügt diese zur Datei application.js hinzu.
- Fügt das Gem image_processing für die Analyse und Bearbeitung der eingebetteten Bilder und anderer Anhänge mit Active Storage hinzu.
Weitere Informationen hierzu im Leitfaden: Active Storage – Überblick. - Fügt Migrationen hinzu, um die folgenden Tabellen zu erstellen, in denen Rich-Text-Inhalte und Anhänge gespeichert werden:
action_text_rich_texts, active_storage_blobs, active_storage_attachments, active_storage_variant_records. - Erstellt die Datei actiontext.css, die alle Trix-Stile und Überschreibungen enthält.
- Fügt die Standard-View-Partials _content.html und _blob.html hinzu, um Action-Text-Inhalte bzw. Active-Storage-Anhänge (auch bekannt als Blobs) darzustellen.
Anschließend werden durch die Ausführung der Migrationen die neuen Tabellen action_text_* und active_storage_* zu der App hinzugefügt:
$ bin/rails db:migrate
Wenn die Action-Text-Installation die Tabelle action_text_rich_texts anlegt, verwendet diese eine polymorphe Beziehung, damit mehreren Modellen "Rich-Text-Attribute" hinzugefügt werden können. Dies geschieht über die Spalten record_type und record_id, in denen jeweils der ClassName des Modells und die ID des Datensatzes gespeichert werden.
Bei polymorphen Assoziationen kann ein Modell über eine einzige Assoziation zu mehr als einem anderen Modell gehören.
Weitere Informationen hierzu kann im Leitfaden zu Active-Record-Assoziationen nachgelesen werden.
Model mit Trix Attribute erweitern
Der RichText-Datensatz enthält den vom Trix-Editor erzeugten Inhalt in einem serialisierten Body-Attribut. Diese enthält außerdem alle Verweise auf die eingebetteten Dateien, die mithilfe von Active Storage gespeichert werden. Der Datensatz wird dann mit dem Active-Record-Modell verknüpft, das Rich-Text-Inhalte enthalten soll.
Die Verknüpfung erfolgt durch Einfügen der Klassenmethode has_rich_text in das Modell, dem Rich Text hinzugefügt werrden soll.
Die Verknüpfung erfolgt durch Einfügen der Klassenmethode has_rich_text in das Modell, dem Rich Text hinzugefügt werrden soll.
# app/models/article.rb
class Article < ApplicationRecord
has_rich_text :body
end
Es ist nicht notwendig, die Spalte body zu der Article-Tabelle hinzuzufügen. Has_rich_text verknüpft den Inhalt mit der erstellten Tabelle action_text_rich_texts und verbindet ihn wieder mit Ihrem Modell. Das Attribut kann auch anders als body benannt werden.
Sobald Sie die Klassenmethode has_rich_text zum Modell hinzugefügt wurde, kann die entsprechende Ansicht angepasst werden, um den Rich-Text-Editor (Trix) für dieses Feld zu nutzen.
Verwendet wird dazu ein "rich_textarea" Element für das Formularfeld.
Verwendet wird dazu ein "rich_textarea" Element für das Formularfeld.
Trix in die entsprechende Ansicht integrieren
<%# app/views/articles/_form.html.erb %>
<%= form_with model: article do |form| %>
<div class="field">
<%= form.label :body %>
<%= form.rich_textarea :body %>
</div>
<% end %>
Dadurch wird ein Trix-Editor angezeigt, mit dem der Rich-Text entsprechend erstellt und aktualisiert werden kann.
Trix Attribute im Controller zulassen
Um sicherzustellen, dass die Aktualisierungen aus dem Editor übernommen werden können, muss schließlich das "referenzierende" Attribut als Parameter im entsprechenden Controller zugelassen werden:
def article_params
params.expect(article: [
:header,
:header_type,
:sorting,
:published_at,
:hidden,
:body # das neue, zusätzliche body Attribute
])
end
end
Controller Action Update
Die Verwendung der Parameter im Controller sieht dann in etwa so aus:
# PATCH/PUT /articles/1
# PATCH/PUT /articles/1.json
def update
respond_to do |format|
if @admin_article.update(article_params)
format.html { redirect_to @admin_article, success: t(:object_updated_successfully, object_name: Article.model_name.human) }
format.json { render :show, status: :ok, location: @admin_article}
else
format.html { render :edit, status: :unprocessable_content }
format.json { render json: @admin_article.errors, status: :unprocessable_entity }
end
end
end
Controller Action Create
Die Verwendung der Parameter im Controller sieht dann in etwa so aus:
# POST /articles
# POST /articles.json
def create
@admin_article = Article.new(article_params)
respond_to do |format|
if @admin_article.save
format.html { redirect_to @admin_article, success: t(:object_created_successfully, object_name: Article.model_name.human) }
format.json { render :show, status: :created, location: @admin_article }
else
format.html { render :new, status: :unprocessable_content }
format.json { render json: @admin_article.errors, status: :unprocessable_entity }
end
end
end
Trix - Funktionalität erweitern / anpassen
Die Basisfunktionalität des Editors praktikabel. Jedoch gibt es einige kleine Mängel, die wir im folgend Beitrag anpassen werden.
- Optische Anpassung - Toolbar Icons austauschen.
- Das standard Layout ist nicht responsive.
- Im "Dark Mode" des Browsers ist einiges nicht sichtbar.
- Format (Button) für "Text Underline" fehlt.
- Format (Button) für "Text Highlight" fehlt.
Javascript mit @hotwired/stimulus Controller
Javascript für die zusätzlichen Format (Buttons) "Text Underline" und "Text Highlight" inklusive neuer Material Design Icons. Das ganze wird mit einem @hotwired/stimulus Controller erledigt. Die entsprechenden Material Design Icons müssen natürlich in der App auch installiert sein.
/**
* Author: Matthias Karr <no-reply@x11media.com> - X11MEDIA
* File: app/javascript/controllers/trix_extend_controller.js
* Created on: 2026.02.24, 09:51
* Last updated on: 2026.05.27, 20:42
* Version: 1.0.4
*
* Most info from: https://github.com/basecamp/trix
*
* Copyright (c) 2026, x11Media - Matthias Karr.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
import { Controller } from "@hotwired/stimulus";
import MyLogger from 'utils/myLogger';
let l = new MyLogger("trix_extend_controller", false);
// Connects to data-controller="trix-extend"
export default class extends Controller {
buttonConfig = [
{ ident: '.trix-button--icon-bold', icon: '<i class="material-icons">format_bold</i>' },
{ ident: '.trix-button--icon-italic', icon: '<i class="material-icons">format_italic</i>' },
{ ident: '.trix-button--icon-strike', icon: '<i class="material-icons">strikethrough_s</i>' },
{ ident: '.trix-button--icon-link', icon: '<i class="material-icons">link</i>' },
{ ident: '.trix-button--icon-underline', icon: '<i class="material-icons">format_underline</i>' },
{ ident: '.trix-button--icon-mark', icon: '<i class="material-icons">priority_high</i>' },
{ ident: '.trix-button--icon-heading-1', icon: '<i class="material-icons">format_size</i>' },
{ ident: '.trix-button--icon-quote', icon: '<i class="material-icons">format_quote</i>' },
{ ident: '.trix-button--icon-code', icon: '<i class="material-icons">code</i>' },
{ ident: '.trix-button--icon-bullet-list', icon: '<i class="material-icons">format_list_bulleted</i>' },
{ ident: '.trix-button--icon-number-list', icon: '<i class="material-icons">format_list_numbered</i>' },
{ ident: '.trix-button--icon-decrease-nesting-level', icon: '<i class="material-icons">format_indent_decrease</i>' },
{ ident: '.trix-button--icon-increase-nesting-level', icon: '<i class="material-icons">format_indent_increase</i>' },
{ ident: '.trix-button--icon-attach', icon: '<i class="material-icons">attach_file</i>' },
{ ident: '.trix-button--icon-undo', icon: '<i class="material-icons">undo</i>' },
{ ident: '.trix-button--icon-redo', icon: '<i class="material-icons">redo</i>' }
]
connect() {
this.configTrix();
l.log('connect()');
// wait for the trix editor is attached to the DOM to do stuff
addEventListener("trix-initialize", (ev) => {
l.log("trix-initialize");
let attributes = {}
let newButton = {}
const trixToolBar = document.querySelector(".trix-button-row");
// add button to toolbar - inside the text tools group
const textTools = trixToolBar.querySelector(".trix-button-group--text-tools");
if (!textTools.querySelector('.trix-button--icon-underline')) {
// create underline button
attributes = { attr: 'underline', key: 'u', title: 'Underline', class: 'trix-button--icon-underline', html: 'Underline' }
newButton = this.createButton(attributes);
textTools.appendChild(newButton);
}
if (!textTools.querySelector('.trix-button--icon-mark')) {
// create mark button - Replaces the standard code button function, later we create a new code button with syntax highlight options.
attributes = { attr: 'mark', key: 'm', title: 'Text Highlight', class: 'trix-button--icon-mark', html: 'Mark' }
newButton = this.createButton(attributes);
// add button to toolbar - inside the block tools group
textTools.appendChild(newButton);
}
// update all toolbar icons
this.overrideIcons(trixToolBar, this.buttonConfig);
}, true);
}
/**
* Trix configuration must be done before addEventListener("trix-initialize", ...
*/
configTrix() {
// initialize header attributes and set h1 to h3
// Musst be allowed by rails: ActionView::Base.sanitized_allowed_tags.add("h3")
Trix.config.blockAttributes.heading1.tagName = 'h3';
// initialize underline attribute
// Musst be allowed by rails: ActionView::Base.sanitized_allowed_tags.add("u")
Trix.config.textAttributes.underline = {
tagName: 'u'
};
// Musst be allowed by rails: ActionView::Base.sanitized_allowed_tags.add("mark")
Trix.config.blockAttributes.mark = {
tagName: 'mark'
};
}
overrideIcons(toolBar,icons){
icons.forEach(button => {
//l.log("overrideIcons: button =>", button)
toolBar.querySelector(button.ident).innerHTML = button.icon;
});
}
/**
* Method for create a new trix editor button.<br>
* @param attributes Object {
* id : myCustomID,
* attr(data-trix-attribute) : 'code',
* key(data-trix-key): 'pre',
* title: 'Pre',
* class(trix-button): 'trix-button--icon-pre' ,
* html(innerHTML): 'Pre'
* }
* @returns {HTMLButtonElement}
*/
createButton(attributes){
const el = document.createElement('button');
el.setAttribute("type", 'button');
if (attributes.id != null) {
el.setAttribute('id', attributes.id);
}
if (attributes.action != null) {
el.setAttribute('data-trix-action', attributes.action);
}
el.setAttribute('data-trix-attribute', attributes.attr);
if (attributes.key != null) {
el.setAttribute('data-trix-key', attributes.key);
}
el.setAttribute('tabindex', '-1');
el.setAttribute('title', attributes.title);
el.classList.add('trix-button', attributes.class);
if (attributes.html != null) {
el.innerHTML = attributes.html;
}
return el;
}
disconnect() {
l.log('disconnect()');
};
}
Stylesheets für den Trix Editor anpassen bzw. überschreiben
Folgende CSS Dateien müssen angepasst werden bzw. erstellt werden:
- app/assets/stylesheets/actiontext.scss - Überschreiben der Icons und einige standard Styles des Trix Editors.
- app/assets/stylesheets/trix.scss - Neue Design definieren. Die originale trix.ccs wird nicht mehr verwendet.
app/assets/stylesheets/actiontext.scss
.trix-content action-text-attachment .attachment {
padding: 0 !important;
max-width: 100% !important;
}
/* === overrides for the new icons === */
.trix-button-row .trix-button--icon-bold::before,
.trix-button-row .trix-button--icon-italic::before,
.trix-button-row .trix-button--icon-bullet-list::before,
.trix-button-row .trix-button--icon-number-list::before {
background-image: none;
}
.trix-button-row .trix-button {
background-image: none; // you may need an !important
//border: 1px solid red;
}
.trix-button-row .trix-button svg {
height: 100%;
float: left;
}
.trix-button-row .ql-stroke {
fill: none;
stroke: #444;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2
}
.trix-button-row .trix-button--icon-bold .ql-stroke {
stroke-width: 2.5;
}
app/assets/stylesheets/trix.scss
@charset "UTF-8";
@use "settings" as defaults;
/* === Trix Toolbar Styles === */
trix-toolbar {
position: relative;
.trix-button-group-spacer {
// border: 1px solid #f00;
//width: 1rem;
display: none;
}
.trix-button-row {
display: flex;
flex-wrap: wrap;
// justify-content: space-between;
.trix-button-group {
// border: 1px solid #f00;
display: block;
float: left;
margin-bottom: 0.3rem;
&.trix-button-group--text-tools {
border: 1px solid #ccc;
margin-right: 1rem;
}
&.trix-button-group--block-tools{
border: 1px solid #ccc;
margin-right: 1rem;
}
&.trix-button-group--file-tools{
border: 1px solid #ccc;
margin-right: 1rem;
}
&.trix-button-group--history-tools{
border: 1px solid #ccc;
}
}
.trix-button {
white-space: normal;
padding: 0.5rem;
margin: 0;
display: inline-block;
position: relative;
&:hover {
color: defaults.$primary;
background-color: defaults.$grey-lighter;
}
&.trix-active {
background-color: defaults.$yellow-light;
}
&:disabled {
color: rgba(0, 0, 0, 0.125);
cursor: not-allowed;
&:hover{
color: rgba(0, 0, 0, 0.125);
background-color: inherit;
}
}
}
}
/*
=== Trix Dialogs ===
*/
.trix-dialogs {
position: relative;
.trix-dialog {
position: absolute;
top: 0;
left: 0;
right: 0;
font-size: 0.75em;
padding: 15px 10px;
background: #fff;
box-shadow: 0 0.3em 1em #ccc;
border-top: 2px solid #888;
border-radius: 3px;
z-index: 5;
.trix-input--dialog {
font-size: inherit;
font-weight: normal;
padding: 0.5em 0.8em;
margin: 0 10px 0 0;
border-radius: 2px;
border: 1px solid #bbb;
background-color: #fff;
box-shadow: none;
outline: none;
-webkit-appearance: none;
-moz-appearance: none;
}
.trix-button-group {
flex: 0 0 content;
margin: 0;
.trix-button--dialog {
font-size: inherit;
padding: 0.5rem;
margin: 0.5rem;
}
}
&.trix-dialog--link {
max-width: 600px;
.trix-dialog__link-fields {
display: flex;
align-items: baseline;
.trix-input {
flex: 1;
}
}
}
}
}
}
/*
=== Trix editor styles ===
*/
trix-editor {
border: 1px solid #bbb;
border-radius: 3px;
margin: 0;
padding: 0.4em 0.6em;
min-height: 5em;
// outline: none;
[data-trix-mutable]:not(.attachment__caption-editor) {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
[data-trix-mutable] ::-moz-selection,
[data-trix-mutable]::-moz-selection,
[data-trix-cursor-target]::-moz-selection,
[data-trix-mutable] ::selection,
[data-trix-mutable]::selection,
[data-trix-cursor-target]::selection {
background: none;
}
[data-trix-mutable].attachment__caption-editor:focus::-moz-selection,
[data-trix-mutable].attachment__caption-editor:focus::selection {
background: highlight;
}
[data-trix-mutable].attachment.attachment--file {
box-shadow: 0 0 0 2px highlight;
border-color: transparent;
}
[data-trix-mutable].attachment img {
box-shadow: 0 0 0 2px highlight;
}
.attachment {
position: relative;
}
.attachment:hover {
cursor: default;
}
.attachment--preview .attachment__caption:hover {
cursor: text;
}
.attachment__progress {
position: absolute;
z-index: 1;
height: 20px;
top: calc(50% - 10px);
left: 5%;
width: 90%;
opacity: 0.9;
transition: opacity 200ms ease-in;
}
.attachment__progress[value="100"] {
opacity: 0;
}
.attachment__caption-editor {
display: inline-block;
width: 100%;
margin: 0;
padding: 0;
font-size: inherit;
font-family: inherit;
line-height: inherit;
color: inherit;
text-align: center;
vertical-align: top;
border: none;
outline: none;
-webkit-appearance: none;
-moz-appearance: none;
}
.attachment__toolbar {
position: absolute;
z-index: 1;
top: -0.9em;
left: 0;
width: 100%;
text-align: center;
}
.attachment__metadata-container {
position: relative;
}
.attachment__metadata {
position: absolute;
left: 50%;
top: 2em;
transform: translate(-50%, 0);
max-width: 90%;
padding: 0.1em 0.6em;
font-size: 0.8em;
color: #fff;
background-color: rgba(0, 0, 0, 0.7);
border-radius: 3px;
}
.attachment__metadata .attachment__name {
display: inline-block;
max-width: 100%;
vertical-align: bottom;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.attachment__metadata .attachment__size {
margin-left: 0.2em;
white-space: nowrap;
}
}
Damit die richtigen Icons auch angezeigt werden, müssen natürlich auch die entsprechenden Material Design Icons in der App integriert sein.
Hier geht es weiter mit dem Thema: Material Design Icons selbst hosten.