You are here: Editor project page > How the Editor Works
How the Editor Works
In this document
Introduction
This document describes how the editor is instantiated, and handles events, for the Composer window. This is very similar to its use in the mail compose window, and will relate to most embedding applications. For details on how the editor is created for text widgets, see the text widgets doc.
The high-level picture
A high-level picture of some of the main parts of the editing system are shown in Figure 1. This picture shows the interactions between the UI (written in XUL and JavaScript), the nsEditorShell (which interfaces between the UI and the editor core), and the editor proper, with typing rules which modify behavior. The editor then acts upon the document being edited. (Note that the figure leaves out some important details, like the fact that all editing operations occur via undoable transactions.)
Figure 1: high-level relationships between editor components in the composer window.
Solid arrows indicate common interaction paths, open arrows less important paths.
Some aspects of this picture deserve comment.
- First note that all the interaction between XUL/JS goes via the
nsEditorShell
. The lower-level editor interfaces are not (yet) exposed to JavaScript via IDL. -
Second, note that the editor core (
nsEditor
/nsHTMLEditor
) knows nothing of thensEditorShell
. The editor is agnostic to who is driving it, and it's not tied to any specific front-end or environment. - Third, note that the document being edited does not know that it is being edited. Editor acts on the document, but the document doesn't need to know anything about the editor.
Editor instantiation in a XUL window
The editor in XUL lives on top of a XUL <iframe>
element; it observes
document loading in this <iframe>
, and, when document loading is
complete, it instantiates an editor on the loaded document. The <iframe>
contents are then editable.
The job of observing doc loading in the <iframe>
is performed
by nsEditorShell
, a class which implements all of the communication between
the JavaScript and the editor core. nsEditorShell
implements the
nsIEditorShell
interface, which is exposed to JavaScript via IDL.
The editor shell creates the editor when the time is right, holds the owning reference
to the editor object, and releases it when the XUL window is closed.
Note: We are currently transitioning to a new tag specifically for editor, called, not surprisingly,
<editor>
. This is really just an <iframe>
which
takes over some of the task of creating the editor from JavaScript. As a result, some of the code
fragments here may be slightly different from what you see in the codebase.
So let's trace through the process of editor creation when bringing up the composer window.
You can find the relevant XUL parts in
editor.xul,
and the JavaScript parts in
editor.js
.
-
Something, somewhere, tells Mozilla to open the composer window.
editor.xul
is loaded. Ineditor.xul
, the<window>
tag has an onload handler:onload="EditorOnLoad()"
. That causes theEditorOnLoad()
JavaScript function to get executed when the XUL is done loading.The XUL contains an
<editor>
or<iframe>
tag. e.g.:<editor type="content-primary" id="content-frame" src="about:blank" flex="1"/>
The attribute
type="content-primary"
identifies this as the window content element, i.e. that which you get fromwindow._content
. Having anid
attribute,id="content-frame"
, allows us to find this element withdocument.getElementById("content-frame")
, and to style it from CSS. -
EditorOnLoad()
is called. It does some getting of window.arguments (which is a way callers can pass parameters to new windows -- we use this to get the URL to be loaded), then it callsEditorStartup()
, where the real work happens. It passes two parameters; the first indicates whether we want a plain text or HTML editor (pass'text'
or'html'
here), and the second is the<iframe>
element on which we wish to create the editor. We could either passdocument.getElementById("content-frame")
orwindow._content
here. -
The important stuff in
EditorStartup()
begins where we get or create an editorShell. Because of the<editor>
transition, you'll see two patterns here:<editor>
version:
The
<editor>
tag actually creates an nsEditorBoxObject behind the scenes. The nsEditorBoxObject creates an nsEditorShell, and holds the owning reference to it. Through the magic of XBL, the XUL bindings, and the nsIEditorBoxObject interface, you can get a JS reference to the editorShell from the editor element withelement.editorShell
. We thus have an editorShell to play with.<iframe>
version:
In the absence of an
<editor>
tag, we have to make the editorShell by hand in the JS:var editorShell = Components.classes["component://netscape/editor/editorshell"].createInstance(); editorShell = editorShell.QueryInterface(Components.interfaces.nsIEditorShell);
Again, now we have an editorShell to play with.
-
Now we set up the editorShell by calling its Init() method, telling it what type of editor we want (text or HTML), pointing it at the webShellWindow to use, and telling it the content node that it lives on:
editorShell.Init(); editorShell.SetEditorType(editorType); editorShell.webShellWindow = window; editorShell.contentWindow = window._content;
The
webShellWindow
(a settable attribute on nsIEditorShell) points to the top-level window element, from which the editorShell can get the XUL document in which it is living. It needs this to poke the UI (e.g. for command state maintenance, starting and stopping the throbber etc.).The
contentWindow
(another settable attribute on nsIEditorShell) points to the XUL element which is to become editable. [Note: since we already know this when we have an<editor>
tag, we should remove the need to call this.] -
EditorStartup() does some other minor bits of setup before finally kicking off the URL load, which the most important part here.
When the XUL was parsed, the
src
attribute on the content frame was set toabout:blank
(our default 'blank page' URL). We can't set that before XUL parsing, so we have to force a load of the page we now want to edit. We get the URL to load from theargs
element, then kick off the load:var url = document.getElementById("args").getAttribute("value"); editorShell.LoadUrl(url);
Loading the document in the
<iframe>
of course happens asynchronously, so we need to know when we have a document that we can start editing.nsEditorShell is able to observe the document load on the
<iframe>
, because it implementsnsIDocumentLoaderObserver
, and registered itself as a doc loader when it was assigned the content window. It thus gets callbacks for the start, progress, and end of the document load.these callbacks also fire for every subdocument that loads as a result of the parent document load, for example with frameset documents, or HTML documents with their own embedded
<iframe>
s. In this case, we need to be careful to instantiate the editor on the correct document. We are currently only able to have one editor per composer window; in future, relaxing this restriction would allow us to edit all the subdocuments in a frameset at the same time.We detect that the document we want to edit has loaded successfully in
nsEditorShell::OnEndDocumentLoad()
. After checking that we can actually edit this document, we go ahead and instantiate an editor on it (innsEditorShell::PrepareDocumentForEditing()
). As well as making the editor (which happens viansEditorShell::DoEditorMode()
) we also hook up various listeners and observers for UI updating and user interaction, and store a file specifier for the document we opened.The editor is now set up, and ready to go.
One thing to note about editor initialization is that we pass into the editor's
Init()
method annsIContent*
that corresponds to the root of the content tree that the editor is allowed to work with. When initializing the editor from the nsEditorShell, we passNULL
here (which tells the editor that it can edit everything under the<body>
of the document). This parameter is more important when the editor is in a text widget, where it points to the the subtree of the parent document that corresponds to widget content.
Editor teardown
Window destruction, and hence editor teardown is initiated in two ways, listed
below. In both cases, the EditorCanClose()
method is the JavaScript
is called, which causes the nsEditorShell to display a dialog asking the user
if they want to save the document, throw away their changes, or cancel. Note that
if they cancel, the close operation is aborted.
-
The user clicks the Close widget in their OS/window manager. In this case,
the
onclose
method on the<window>
tag is called. -
The user chooses 'Close' from the File menu, uses the key shortcut,
or quits the application, causing all windows to be closed. Before each
window is closed, JavaScript code in
globalOverlay.js
tries to call a
tryToClose
method on each window. Ineditor.js
, we set this to callEditorCanClose()
.
If the user chooses to save the document, or throw away their changes,
then the window is closed. When the last reference to the
nsEditorShell
goes away (either as a result of JavaScript garbage collection in the
<iframe>
case, or the nsEditorBoxObject releasing its reference in the
<editor>
case) it releases the owning reference on the editor.
Editor event handling
Editing operations happen in response to user events: mouse, key, drag and drop, and IME (international text input) events. In order to receive these events, the editor registers several event listeners on the document being edited. In addition, editor actions in the user interface are propagated via the XUL and JavaScript, and call methods on the nsEditorShell. This editor command dispatching is described separately.
The following event listeners are registered:
-
In
nsHTMLEditor::InstallEventListeners()
, we install the following. These get installed for all types of editor (i.e. for text widgets and composer):nsTextEditorKeyListener
(as ansIDOMKeyListener
)nsTextEditorMouseListener
(as ansIDOMMouseListener
)nsTextEditorFocusListener
(as ansIDOMFocusListener
)nsTextEditorTextListener
(as ansIDOMTextListener
)nsTextEditorCompositionListener
(as ansIDOMCompositionListener
)nsTextEditorDragListener
(as ansIDOMDragListener
)
-
In
nsEditorShell::PrepareDocumentForEditing()
, we install a mouse listener. This only happens for situations where the nsEditorShell is used (i.e. not for text widgets):nsEditorShellMouseListener
(as ansIDOMMouseListener
)
nsTextEditorKeyListener
This event listener handles key presses for typing, and other editing operations (backspace, delete, enter/return).
Cases that it does not handle explicitly it passes on to nsHTMLEditor::EditorKeyPress()
,
which is where normal typing keys end up. Note that it only responds to the KeyPress
event;
KeyDown
and KeyUp
events are ignored.
nsTextEditorMouseListener
The mouse listener is used to do middle-mouse paste (which is a Unix copy/paste feature). This happens
in response to MouseClick
with button 2. It also forces an IME commit.
nsTextEditorFocusListener
Editor responds to Focus
and Blur
events by showing and hiding the caret or selection as appropriate.
nsTextEditorTextListener
The nsIDOMTextListener
interface that this implements is used by the IME code. In response
to the HandleText
event, the editor sets the inline input composition string.
nsTextEditorCompositionListener
nsTextEditorCompositionListener
implements another IME-related interface,
nsIDOMCompositionListener
. This is called by IME at the start, end, and to query the current
composition.
nsTextEditorDragListener
The drag listener handles drag and drop events in the editor. It responds to the start of a drag in
DragGesture
by adding data to the drag, notifies the drag whether a drop can occur in
DragOver
, and handles the drop by inserting data in DragDrop
.
nsEditorShellMouseListener
This is an odd-man-out event listener, in that it's registered from the editorShell, rather than
internally to the editor. [Note: this is ugly, and should probably be redesigned to work on callbacks out of
the editor, or moved entirely to JavaScript.] The nsEditorShellMouseListener
essentially
calls nsEditorShell::HandleMouseClickOnElement
to show property dialogs for items that you double-click on.
The path of a key press
So what happens to a key press once it's got to the nsTextEditorKeyListener
? How does that
end up in the document? Let's trace through.
-
nsTextEditorKeyListener::KeyPress()
gets the key press event. For normal character keys, that falls intonsHTMLEditor::EditorKeyPress()
. -
nsHTMLEditor::EditorKeyPress()
gets the character code from the key event, puts that into a string, and callsnsHTMLEditor::TypedText()
, which simply callsnsHTMLEditor::InsertText()
. -
nsHTMLEditor::InsertText()
hides quite a bit of complexity in some stack-based classes.nsAutoPlaceHolderBatch
is a utility class that wraps text insertion with calls to turn off selection and layout updating (to avoid flicker), and the maintenance of a placeholder transaction. This placeholder transaction enables us to batch typing events together, so that an Undo undoes the whole series of keystrokes.Another stack-based class,
nsAutoRules
, ensures that text insertion is wrapped with calls tonsHTMLEditor::StartOperation()
/EndOperation()
. These functions callBeforeEdit()
andAfterEdit()
on the current typing rules.Now, we initialize a
nsTextRulesInfo
with the information about the string being inserted, and callWillDoAction()
on the current editing rules. Because the implementation of inserting text differs between the different rules (plain text vs. HTML, for example), it is handled entirely by the rules code, in theWillDoAction()
call.In Composer, we are using
nsHTMLEditRules
, so we end up innsHTMLEditRules::WillDoAction()
. For text insertion, this drops intonsHTMLEditRules::WillInsertText()
. This code first deletes the selection if there is one (e.g. you are typing over selected text), then calls a generic pre-insertion callWillInsert()
, which sets up inline styles for the inserted text, and moves the selection to an appropriate place where the text is to be inserted.Now we are ready to actually insert the text. Recall that we're going through a generic
InsertText()
call, so this code deals with pasting long strings, as well as inserting single characters. The code thus has to do the correct thing with linebreaks, so has a special case for inserting into<pre>
sections. We call into the normal insertion code, which loops through the input string looking for linebreaks, and inserts each text run, followed by a<br>
when necessary. When handling key presses, this will just insert a single character.We fall out of the
WillDoAction()
call, and drop intoWillDoAction()
, which, for text insertion, does nothing.The last thing that happens on a keypress is that
nsTextEditorKeyListener::KeyPress()
callsScrollSelectionIntoView()
, which, as the name suggests, ensures that the text that was just entered is visible.