Editing

Once Mozile has been loaded, it acts in response to events triggered by the user. These can be mouse events, like clicking on a part of the document or a button, or keyboard events like typing or entering a command shortcut. Mozile listens for these events, and then uses them to trigger commands which make changes to the document.

Events

Events are part of the Document Object Model (DOM) defined by the W3C. Although there are differences between browsers in the way they handle events, the essentials are the same. The DOM is a representation of an HTML or XML document as a tree made up of nodes. There are many kinds of nodes, but we're usually only concerned with Text nodes and Element nodes, and sometimes Attr or "attribute" nodes. Any text displayed in the document will belong to a text node, and text nodes are the children of elements. Elements can contain text nodes as well as other elements. (We usually use the words "element" and "tag" as synonyms.) Every node has a parent node, until we get to the "root" of the tree, which is the documentElement. In HTML this is the html element. The parent of the documentElement is the document, and the document has no parent.

An event like a mouse click will start at the node that was clicked and travel up the tree from child to parent until it reaches the root. Keyboard events start at the documentElement or the body or the contentEditable container, depending on the browser, but they also work their way toward the root.

In order to catch the events as they pass through the DOM tree and do something about them, we need to assign "event listeners". The mozile.event.listen() method, called at the end of src/event.js, creates the event listeners and attaches them to the document.

Note

There are many different event types, such as "mouseclick" and "keypress", and mozile.event.listen() only assigns listeners for some of these. If you have a command that you want triggered by a certain event type, make sure that that type is included by mozile.event.listen().

The event listeners listen for the events, and when they "hear" one they pass the event object to mozile.event.handle(). mozile.event.handle() then does several things. It checks to see if the event took place in an editable area, which means that the original target node of the event has an ancestor with contentEditable="true". It can also pass the event to other functions and let them try to handle it. An event is "handled" when it triggers an appropriate action, like a command. Once the event is handled it will be cancelled using mozile.event.cancel(), and that will be the end of it. The event will stop moving through the DOM tree, and no other code will see it. If Mozile doesn't handle an event it will let the event keep travelling on its way.

The most important thing that events do is trigger commands, but there are a few more things to explain before we get to Mozile's command system.

Selection

A mouse click event will have an event.target property which indicates which node was clicked. Keyboard events don't have this property, so instead we determine the target node using the document's Selection object. The selection is displayed as a cursor or as highlighted text in the document.

Most of the time, browsers don't display a cursor in a web page. The arrows keys will move the whole page up and down. But when Internet Explorer detects the contentEditable attribute, or Firefox is in designMode, a cursor will appear. In Firefox you can also enable a cursor by pressing F7 to activate "Caret Browsing", which is an accessibility feature. In order to have a cursor displayed automatically, Mozile uses contentEditable and designMode. But that's all they are used for; the acutal editing operations are done using the DOM.

A selection can include a range of text and elements, or it can be "collapsed" to a single point. Related to the selection object is the Range object. Every selection contains at least one range, but a range does not have to be selected. The beginning and end points of a range or a selection are indicated by the pair of a node and an offset, which is an integer indicating where the point is within that node. In the case of a text node, the offset will be a number of characters, so you can select the third character of the text node. In the case of an element node, the offset will be the number of child nodes, so you can select the third child of the element node.

This description of nodes and offsets applies to Firefox and other browsers that comply with the W3C standard for ranges and selections. Internet Explorer has its own way of handling ranges and selections, which is incompatible. After a lot of work we've managed to make Internet Explorer behave according to the standards, for the most part. For the code that does this, see src/dom/InternetExplorerRange.js and src/dom/InternetExplorerSelection.js.

When mozile.event.handle() receives a keyboard event it looks for the current selection and uses the first node which contains both the start and the end of the selection as the target of the event. Once this is done, keyboard and mouse events are treated in the same way.

Mozile's mozile.edit.InsertionPoint (IP) object is related to ranges and selections. It also uses a node and an offset to indicate a location in the document. However, the insertion point code knows a lot more than the normal range and selection objects do about how to handle white space and which elements are allowed to contain text. So some of Mozile's code just uses the range and selection objects, while other code uses insertion points for finer control.

RelaxNG

Once mozile.event.handle() knows which node is the target of the current event, it needs to decide how to handle the event. Mozile is a context sensitive editor, which means that it's smart enough to tell the difference between editing a paragraph and editing a list, for example. We call this "structured editing"; Mozile knows about the way documents are structured, and helps maintain that structure as changes are made.

Mozile is sensitive to the structure of documents, but you have to tell it what the structure should be. We do this using a "schema language" called RelaxNG (RNG). All you have to do is tell Mozile where to find an RNG file for the document type you want to edit, and Mozile will do the rest.

Here is an example of how RNG describes the structure of an a element:

Example 2.2. A RelaxNG definition

<define name="a">
  <element name="a">
    <attribute name="href"/>
    <text/>
    <ref name="Inline.model"/>
  </element>
</define>

This bit of RNG says that an a element must have an href attribute, and that it's allowed to contain non-white-space text. It also refers to another definition named "Inline.model", which will have more details about what the a element is allowed to contain. RNG files can also contain Mozile Editing Schema information, which adds details about the commands associated with various elements. See below for more details.

After mozile.event.handle() finds the target of an event, it looks for a RNG representation of that node. This will be a mozile.rng.Element object. If the node is a text node, it uses the parent element instead. Mozile stores all sorts of information from the RNG schema about the document's elements, and this includes what commands are associated with what kinds of elements.

So mozile.event.handle() takes an event, figures out the target node, and then figures out what mozile.rng.Element object matches up with the target node. Once it has this information, it calls the mozile.rng.Element.handleEvent() method, and that method calls the commands associated with the mozile.rng.Element.

Note

Mozile 0.8 will also support the validation nodes and documents against RelaxNG schemas, however the validation system is not yet complete.

Default Editing Commands

It is not necessary to use a RelaxNG schema in order to edit documents with Mozile. mozile.event.handle() first tries to use the RNG system, and if the event has not been handled it will test a set of default editing commands.

Default editing commands can be added using the mozile.edit.addDefaultCommand() method, and they are handled using the mozile.edit.handleDefault() method. However the easiest way to add a good selection of default editing commands is to use the mozile.editAllText(rich) command in your configuration. If rich is false then commands for inserting and removing text will be added as default editing commands. This means that elements cannot be created or destroyed, and only the text that they contain can be changed. If rich is true then text editing commands and commands for spitting, merging, and removing elements will be added.

In most cases using an RNG schema is recommended, because without an RNG schema to guide its actions Mozile will not be able to maintain the validity of the document. For example, Mozile will not be able to tell which elements are allowed to contain non-white-space text and which are not allowed. However there are cases where a schema is unavailable or undesirable, and the default commands serve that purpose.

Commands

All of Mozile's editing operations are performed by commands. There is a mozile.edit.Command "class", and several sub-classes with specialized functions. ("Class" is put in quotation marks because JavaScript doesn't have classes in the same sense that Java does. However, for most of our purposes they behave the same.) Commands can be executed and "unexecuted", so every editing operation can be done, undone, and redone. If you decide to undo an editing command, the document will be in exactly the same state as it was before you called the command in the first place.

Commands can be associated with mozile.rng.Element objects, such as text or element insertion commands, or with the document as a whole, like the Undo and Redo commands. They can also be associated with the GUI. Every command has a unique name, and all of the commands are stored in an associative array named mozile.edit.allCommands.

Every command has the following methods:

  • respond() - given a change code, determines whether the command should be updated. See below for notes on change codes and the makesChanges and watchesChanges properties.
  • isAvailable() - given an event, determines whether the command is available for use in the current editing context.
  • isActive() - given an event, determines whether the command is active or disabled in the current editing context.
  • test() - given an event or other arguments, determines whether the command can be executed.
  • trigger() - takes an event and if the test is successful it executes the command. Use this method to activate a command as the result of a user event.
  • request() - takes a state and some arguments and if the test is successful it executes the command. Use this method when calling one command from another command.
  • prepare() - used to extract information from the event object and other arguments and store it in a mozile.edit.State object. The state object contains enough information for the command to be executed and unexecuted, and the state is stored by the undo system.
  • execute() - takes the state object and does the actual work of manuipulating the document.
  • unexecute() - takes the state object and undoes everything which execute() did, leaving the document exactly as it was.

There are other methods and properties which we will ignore for the time being. See below for more details.

The mozile.edit module defines a few basic text editing commands. The mozile.edit.rich module defines more primitive commands for editing text and elements. Very powerful commands can be created simply by combining these primitive commands. Here is a list of some commands built in to Mozile (they are all stored in mozile.edit.*):

  • insertText
  • removeText
  • insertNode
  • removeNode
  • moveNode
  • remove removes combinations of text and elements
  • mergeNodes
  • splitNode
  • splitNodes

Here are the classes of commands that are available (they are all stored in mozile.edit.*):

  • Command is a generic command
  • CommandGroup groups commands together, but does not perform editing operations.
  • Navigate moves the selection through the document.
  • Split splits an element and its contents in two.
  • Insert inserts either text or an element at the current selection.
  • Wrap wraps the current selection inside one or more new elements.
  • Unwrap removes a target element but keeps all the child nodes.
  • Replace replaces a target element with a new element, without changing the child nodes.
  • Style changes the style of a target element.

These are the building blocks from which you can create powerful editing behaviours.

When mozile.rng.Element.handleEvent() is given an event, it passes the event to the trigger() method for each command on its list of commands. If one of the commands is triggered, then the command is executed, and the resulting state object is returned. That state object is stored in the undo system by mozile.edit.done(). Then the event is cancelled so it will not trigger any more commands.

If none of the commands are triggered, then the whole process is repeated with the target node's parent node. mozile.edit.handle() keeps trying all of the ancestor nodes, until the event triggers a command or the ancestor is no longer inside an editable element. If the event isn't handled by any command then Mozile lets the it continue on its way.

States

mozile.edit.State objects are very important for the command system. A command's prepare() method will prepare a new mozile.edit.State instance containing all of the information needed to execute and unexecute the command. When the command is executed the prepared state is sent to the command's execute() method and then stored by the undo system. When the command is undone, the state is sent to the unexecute() method. When the command is redone, the state is sent to the execute() method again.

Commands may add, remove, and alter the nodes in the document. When a command is undone, the structure of the document will be exactly the same as it was before the command as executed. However, the particular node objects that make up that structure may not be exactly the same as before. In other words, while the XPath addresses of all the parts of the document structure will be the same as before, the identity of particular DOM objects may have changed. For this reason the prepare() should almost always store references to nodes using their XPath addresses, which will stay the same, and not references to the node objects, which may change. Failure to do this can lead to strange bugs which only show up after a Redo operation.

Text and White Space

JavaScript supports Unicode characters, and so does Mozile. The key problem using Unicode is font support. Mozile will insert any Unicode character, but the browser might not be able to render it properly if the available fonts do not include the character. Keep this in mind.

White space is a tricky matter when it comes to web browsers. Like most programming languages, the rules for HTML and XML say that white space characters (like spaces and tabs) are treated differently than normal text. Multiple white space characters are considered equivalent to a single character. This allows you to add tabs and spaces to your HTML source, making it more readable without changing the way in which the document is displayed.

Word processors do not follow the same rules. Since Mozile edits HTML and XML documents in a browser, but it tries to act like a word processor, this creates some problems. Even worse, different browsers handle white space in different ways.

There are two special white space cases that Mozile handles: inline text and blocks. The inline case includes HTML tags like strong and em, while the block case includes p and div. Both of these are values of the CSS display property.

The problem with blocks is that Mozilla will collapse a block that contain only normal white-space characters. So a p tag that contains a space character will look different than one that includes a non-breaking space character. Mozile handles this case using the mozile.emptyToken. By default this is set to the Unicode non-breaking space character \u00A0. When Mozile makes a change to a block element which removes all the non-white-space text, then an empty token will be inserted. If text is later added, the empty token is removed.

The inline case uses the mozile.alternateSpace setting. By default the value is null, and Mozile will ignore events which try to insert multiple consecutive spaces. A maximum of two consecutive spaces will be created by Mozile. This keeps the source code clean, but can cause some strange looking behaviour with the cursor around white space characers. If the alternateSpace is set to \u00A0 (or some other character), then Mozile will apply a somewhat complicated set of rules to insert space characters and alternate space characters when the user presses the space bar. Normally the two different characters will alternate, but sometimes two alternate space characters will be placed consecutively.