-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Implementation
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.
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:
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.
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.
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
.
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
HTMLElement
s needed to display the block.
Trix.PieceView
Trix.DocumentView
Trix.ObjectGroupView
Trix.TextView
Trix.BlockView
-
Trix.AttachmentView
Trix.PreviewableAttachmentView