Skip to content
Richard Michael edited this page Feb 11, 2021 · 27 revisions

Note: This document is maintained by end users of Trix like you. This is a work in progress but you are encouraged to make changes to this document if you discover anything new about Trix.

Since this document is so long, it's easiest if you edit it in a text editor rather than on GitHub. Lines are wrapped to 80 characters in length.

File/Object Structure

The code in Trix may seem daunting at first, but it is actually fairly well organized.

Within src/trix, the three folders that you will want to focus on are controllers, models, and views.

In the section below we'll make references to Trix's object graph. The codebase has a lot of classes, but there is an inherent hierarchy present based on which objects have access to other objects:

Trix object graph

Not included in this graph are the views. We'll talk more about this in Rendering, but know for now that DocumentView is the main object here and it may contain one or more child views, which are instances of some of the other classes present in the views/ directory, and all of them inherit from ObjectView.

Execution Flow

Assuming that you've included the trix.js code on your site, as soon as you add <trix-editor> to your HTML and load the page, the element will be automatically and immediately initialized. How does this work? Trix uses the Custom Elements API to literally define a new element called trix-editor and inform the DOM about its behavior. This happens in elements/trix_editor_element.coffee, which calls registerElement, an internal helper function, with some configuration properties. defaultCSS is a special property that will be used to style the element, but the remaining properties will be given directly to document.registerElement. We will focus on createdCallback and attachedCallback in particular; as these will be called by the DOM immediately after it "sees" the element on the page, this is where our journey really takes off.

In attachedCallback we create an instance of EditorController. This is a very important class, as it serves as the main coordinator for the code that gets called while a user is interacting with a Trix editor. The Trix code is highly event-driven: anything that happens does so as a result of user input -- and there are many different forms of input.

To make this overview less abstract, then, let's cover one use case. Let's examine what happens when the character "A" is typed in the editor.

EditorController creates an instance of InputController. In the constructor for InputController, event handlers are created for each event defined in @events, a giant object definition at the bottom of the class definition containing callbacks for each event type (here, handleEvent is a utility function that uses addEventListener under the hood):

# Create event handlers for all input events
for eventName of @events
  handleEvent eventName, onElement: @element, withCallback: @handlerFor(eventName), inPhase: "capturing"

So, when an event occurs, @handlerFor will be called with the name of that event. This looks like:

handlerFor: (eventName) ->
  (event) =>
    @handleInput ->
      unless innerElementIsActive(@element)
        @eventName = eventName
        @events[eventName].call(this, event)

There are a couple of things that happen here. Let's start with the first: the call to handleInput. That looks like this:

handleInput: (callback) ->
  try
    @delegate?.inputControllerWillHandleInput()
    callback.call(this)
  finally
    @delegate?.inputControllerDidHandleInput()

What is @delegate? The Trix code makes heavy use of the delegation pattern (specifically, in terms of naming for objects and callback functions, it seems to borrow ideas from frameworks like Cocoa), and you'll see a lot of this while source diving into the Trix code. Controllers typically have a delegate object, which is usually another controller higher up the overall object graph.

Here, the delegate of InputController is EditorController, so we jump back there and call inputControllerWillHandleInput:

inputControllerWillHandleInput: ->
  @handlingInput = true
  @requestedRender = false

This doesn't do a whole lot, but setting @handlingInput to true is important, and we'll come back to this.

Back in InputController and handleInput, we call the given callback, which is the function provided in handleFor. That callback contains this line:

@events[eventName].call(this, event)

Here we look up the name of the event in @events and call its handler. In this case, the eventName is keypress and its handler (again, defined at the bottom of InputController) looks like:

...

if character?
  @delegate?.inputControllerWillPerformTyping()
  @responder?.insertString(character)
  @setInputSummary(textAdded: character, didDelete: @selectionIsExpanded())
...

We mentioned before that objects may have a delegate object. They may also have a responder as well, which is like a delegate in that it is another object that can handle what is happening (and it is also another idea pulled from frameworks like Cocoa).

In this case, InputController's delegate is EditorController, and its responder is an instance of the Composition model. Let's skip EditorController#inputControllerWillPerformTyping and look at Composition#insertString instead. Composition, like EditorController, is also a very important class, as it contains a set of commands for making changes to the editor.

We don't have to know exactly what insertString does exactly, except that it calls insertText. Many methods in Composition end up calling this method, so let's take a brief look at it:

insertText: (text, {updatePosition} = updatePosition: true) ->
  selectedRange = @getSelectedRange()
  @setDocument(@document.insertTextAtRange(text, selectedRange))

  startPosition = selectedRange[0]
  endPosition = startPosition + text.getLength()

  @setSelection(endPosition) if updatePosition
  @notifyDelegateOfInsertionAtRange([startPosition, endPosition])

This method takes either the location of the current selection or (if there is no selection) the current location of the cursor and inserts text at that location. However, there are two things to note here. First, there's no mention of the DOM. This is because Trix actually stores an internal state of the editor free from any concept of HTML and then uses that representation to generate HTML that represents that state. That state is stored as a Document object. Second, when you make changes to the editor, you don't change that Document object, you actually replace it. That's what this line is doing:

@setDocument(@document.insertTextAtRange(text, selectedRange))

setDocument looks like this:

setDocument: (document) ->
  ...
  @delegate?.compositionDidChangeDocument?(document)

Composition's delegate is EditorController, and if we jump back to this file, we will see why: after instantiating InputController, it makes a new instance of Composition followed by CompositionController, and passes itself to both as the delegate.

So while we are here, let's look at compositionDidChangeDocument:

compositionDidChangeDocument: (document) ->
  @editorElement.notify("document-change")
  @render() unless @handlingInput

So here we can see that when the document -- the internal representation of the editor -- changes, Trix automatically updates the outward representation of the data in the form of HTML through render, and that's when you see the changes on screen. But note that the call to render is wrapped in a conditional. Here is where that @handingInput variable comes into play, because as it turns out, render isn't called this way when a key is pressed.

How, then is it called? Back in InputController#handleInput. Recall that method:

handleInput: (callback) ->
  try
    @delegate?.inputControllerWillHandleInput()
    callback.call(this)
  finally
    @delegate?.inputControllerDidHandleInput()

Once the callback is called, we will hit the finally branch and call inputControllerDidHandleInput. The delegate to InputController is EditorController, so that's where we need to go:

inputControllerDidHandleInput: ->
  @handlingInput = false
  if @requestedRender
    @requestedRender = false
    @render()

And that's it. After all of this, the letter "A" is finally drawn to the screen.

So what happens in render? A lot of things. But we'll get to that in Rendering.

Event Handling

As mentioned above, the implementation makes heavy use of the delegation pattern.

Events are handled with a combination of code run in InputController and EditorController. In some cases, InputController delegates back to EditorController. EditorController acts as a delegate for the SelectionManager, Composition, InputController, AttachmentManager, and ToolbarController. If delegate?.method is called, chances are you will find the implementation in EditorController.

Rendering

EditorController#render initiates a chain of method calls that result in calling render in the CompositionController, which is handled efficiently by either rendering the DocumentView from scratch or by syncing the changes. (We'll leave a description of sync for later.)

render: ->
  unless @revision is @composition.revision
    @documentView.setDocument(@composition.document)
    @documentView.render() # RENDER the view
    @revision = @composition.revision

  ....

  @delegate?.compositionControllerDidRender?()

To render the DocumentView, the internal representation of the document is converted into DOM elements. These elements are created recursively by rendering the DocumentView and child views.

Trix.ObjectView provides base rendering methods, and there are many subclasses. Each of the subclasses is responsible for creating the HTMLElements needed to display the block.

  • Trix.PieceView
  • Trix.DocumentView
  • Trix.ObjectGroupView
  • Trix.TextView
  • Trix.BlockView
  • Trix.AttachmentView
    • Trix.PreviewableAttachmentView