For years, I’ve tried to answer a simple question: How do you show the same data in two places at once, in Mozilla? In particular, DOM nodes. The simple answer is to clone the data you have in one location, and place the clone in another. This is not enough when the data changes in one location; I want it to change in the other, too.
- A split view of the same document, particularly for editing
- My latest attempt at a scrollable content object model (XBL at work, again)
- Correcting markup in one section of code, and having it update a dependent section later automatically. Think slide shows, where two slides are nearly identical – and a semantic relationship exists between them. Also think steps of a theorem proof.
An ideal solution might involve a special layout frame for transposing the data, but that’s more work in an area I’m unfamiliar with. A solution I can do (more expensive memory-wise, but at least workable) involves setting DOM mutation listeners. Read the extended entry for more details, and a ZIPped source directory for my own synchronizer – if you don’t mind some technobabble.
In short, there are three parts to keeping a pair of nodes synchronized: managing mutation listeners, detecting changes and reflecting them. The mutation listeners exist for the sole purpose of handling the latter two parts.
Gecko 1.9 actually makes most of this pretty easy. With the UserData interfaces of DOM Level 3 Core, I can define references between nodes in one DOM subtree, and equal nodes in a second DOM subtree. DOM Level 2 Traversal helps find every single node in each tree, so I can create the references. DOM Level 2 Events support means I can detect when a change happens and act on it. DOM Level 2 Range provides a way to watch over whole sets of DOM nodes – providing I define another specialized mutation listener to watch over the contents of the range. DOM 3 Core’s isEqualNode method (and the xpcshell test harness) means I can test the scripted synchronization and ensure that it really did update correctly.
The fun begins when you realize all the little ways a synchronizer can go wrong.
For instance, say your synchronizer is watching Node1-Node2 and Node2-Node3. If you’re not careful, a user could define another relationship, Node1-Node3. The result is that Node1 updates Node2, which updates Node3, which updates Node1 again… an endless loop.
Or, say one of the nodes you’re watching is a document fragment. Bad idea. The moment you tell the DOM to append that document fragment to a node, the children of that document fragment will be appended instead. (That’s how document fragments work, and it’s why I’ve always thought of document fragment nodes as a type of DOM clipboard.) The result is that the DOM removes those nodes from the document fragment. The synchronizer then removes the equivalent nodes from the paired node, and you’ve got data loss.
Another nasty scenario is when one node you’re synchronizing contains the matching node. They can’t possibly be equal under that scenario, and it all falls apart. (If the node pairs you’re watching aren’t equal, or the range pairs aren’t equal, what’s the point of trying to synchronize them?)
Finally, if you attach the same mutation listener to both trees, you’d best make sure that when one node mutates and it causes the second node to mutate, that it doesn’t again cause the first to mutate.
Thus, validating the arguments for synchronization is extremely important. If you want to watch ranges of nodes (as I do), the complexity required for validation is then squared.
It also makes sense that if you want to start synchronization at one point, you’ll eventually want to stop it at another. So providing API for detaching the synchronizer from a node pair or range pair is a good idea.
Ranges can change their positions too – so when you start watching a range, you really want to watch a clone of the range which you own (and never change by yourself).
Fortunately, I’ve covered all of these. (I think.)
However, the DOM Synchronizer must also work – and when it stops working, you need a way to find out. For this, I also included a fifth method, hasInternalError(). (In the case of an internal error, all synchronization stops and the four primary methods would all throw an exception.)
The ZIP file also contains a XPCShell test case which beats up on the DOM Synchronizer a bit. I probably need to write more tests for the validation routines…
This code has been tested and implemented for Firefox 3.0 beta 1 and Gecko 1.9b1 equivalents. Known bugs:
###!!! ASSERTION: Event listener manager hash not empty at shutdown!– this one, as far as I can tell, isn’t my fault directly. It happens after the test finishes running, but my test logs show all my event listeners have been accounted for and removed. I need some investigation of that, and I should probably file a bug.
- When a page unloads, and it contains a DOM node the synchronizer watches, by definition the node goes away. The synchronizer still keeps a reference to it, though… and I don’t know how it will react, especially when the matching node hasn’t been unloaded yet. I suspect memory leaks at the least, and possibly even crashes.
I’m sure there are a bunch of possibilities for this synchronizer that I haven’t thought of yet, but it’s a start. I’d appreciate it if anyone still reading this could take a look at it…
UPDATE: The assertion failure is gone on trunk. So are a ton of noisy NS_WARNING calls. Yay!!!