Monday, August 25, 2014

DOMCursor, a tool for filtered DOM tree cursoring

While working on Leisure, I searched for tools to traverse text in DOM trees, but most of the ones I found were fairly limited.  One nice one is Rangy (, which has a TextRange that can traverse by characters and words with rules for skipping over invisible text, collapsing contiguous whitespace, etc. Leisure, however is what you might call an “ultra-rich text” environment and needs more power.   This isn't my announcement of Leisure, by the way -- it's got a bit farther to go for that -- so consider this a teaser :).

Leisure documents are orgmode files and the environment has a couple ways to present them, some of which sprinkle controls and views in among the editable text. During the design, I decided to use the contenteditable attribute and use the text in among the sprinkled controls and views as the actual document text, as opposed to emulating document editing, like code mirror does. Over the years, I experimented with different approaches, such as using shadow DOM, which I eventually abandoned, because of complications and performance issues with importing large CSS files into many shadow trees.

After dumping the shadow DOM approach, I moved those controls and views from the shadow back into the light, marking them with a data attribute (data-nonorg), so I could skip over them. I had a bunch of functions to help with this, so I collected them into a class called DOMCursor, to make it easier to debug maintain.

There are several web-based editors out there, but not much in the way of tools to help people who are bulding their own and I’ve seen posts asking for tools like this. DOMCursor is already open source and part of the Leisure project, but I decided to release it as a separate project, without any Leisure dependencies. And I even went to the trouble to document it!

Here’s the project and here are some details from the source file (which doubles as the readme):


Filtered cursoring on DOM trees. DOMCursors can move forwards or backwards, by node or by character, with settable filters that can seamlessly skip over parts of the DOM.
This readme file is also the code.

DOMCursors are immutable – operations on them return new DOMCursers.
There are two ways to get mutabile cursors, sending @mutable() or
sending @withMutations (m)-> …
A DOMCursor has a node, a position, a filter, and a type.
  • node: like with ranges, a DOM node
  • position: like with ranges, either the index of a child, for elements, or the index of a character, for text nodes.
  • filter: a function used by @next() and @prev() to skip over portions of DOM. It returns
    • truthy: to accept a node but its children are still filtered
    • falsey: to reject a node but its children are still filtered
    • ‘skip’: to skip a node and its children
    • ‘quit’: to end to make @next() or @prev() return an empty DOMCursor
  • type: ‘empty’, ‘text’, or ‘element’
Check out the repository for more info.