2012. szeptember 1., szombat

HTML contenteditable - Going nifty

So, we have arrived to the third experiment:
Making the selection interact with the user. We will detect if there is any selection and if there is any, we will show a nice little modal box aligned to the selection.

ClientRect object

This is a really simple object, it contains the following attributes on the instance level:
  • top
  • left
  • right
  • bottom
  • width
  • height
Each Range object has its own ClientRect object describing the actual range's dimensions and positions. Cool, huh? So how do we use it? Simple:
//returns a ClientRect object
var clientRect = range.getBoundingClientRect();
Using this object we can simply show up a modal dialog box after some short & basic math using jQuery:
//returns a ClientRect object
var clientRect = range.getBoundingClientRect();
menu.css({
    left: (rect.left +
           (rect.width / 2) -
           (menu.outerWidth(true) / 2)),
                
    top: (rect.top +
          rect.height +
          5)
    })
    .show();

Modifying a selection through Range

Mofiying a selection is not a big deal, you only need to keep in mind some rules:
  • Creating a new Range object is done through document.createRange() method.
  • The range.setStart() method does NOT accept HTMLElement as a first parameter. You need to pass a TextNode (#text) type node in there.
    Also, Webkit cannot select empty elements (without at least one TextNode)
    Solution: use the Element.firstChild property instead of the Element - in case you only have text inside the element. Otherwise you can write a function which digs down in the Element's children and their children and returns a TextNode.
  • range.startContainer and range.endContainer contains the TextNodes where the selection begins and ends. They point to the same TextNode if there is no selection or the selection does not flow out from a TextNode.
  • range.startOffset and range.endOffset represent the numbers (positions) where the selection ends. Watch for the positions, they return the "distance" from the beginning of their TextNode, NOT the distance from the parent Element. We used a method to merge these separate TextNodes and another to get the "absolute" position of the caret.
Modifying a selection is quiet simple:
//creating a new range object
var newRange = document.createRange();

//setting up the new range object created at the beginning of the scope
newRange.setStart(theTextNodeYouPreviouslyFound, start);
        
//removeing all range objects and adding the new one
selection.removeAllRanges();
selection.addRange(newRange);
So putting it all together, I have created this small dummy interaction, check it out at this jsfiddle. It is simple, right? =) Just enough for a weekend demonstration. =)

HTML contenteditable - Basics for awesomeness

So, you have already found out the basics of a contenteditable element. Let's focus now on how to work with the Selection and the Range(s) inside.
I will use jQuery for the demostrations because of two reasons:
  • I am used with jQuery...
  • It is just way easier to delegate your eventHandlers with jQuery since your event will bubble (propagate) up on the DOM (watch out with the context!).

Selection object

The Selection object is a single "static" object of a Window object. You can have more Window objects by adding iframes into your site/app and each frame is going to have its own single Selection object, but no more. You might want to be aware of that.
You can get this object by calling the window.getSelection(); method. That will give you back a reference to the Selection object. Again: the Selection object is single, if you reuse it somewhere else in your code, I recommend storing it in your "parent/owner" scope somewhere. Here's a little experiment with the Selection object:
kldfnlkdnjd
​(function () {
    var selection = window.getSelection();
    
    $(document.body)
        .on('click', '[contenteditable]', function () {
            var sel = window.getSelection();
            
            alert(selection === sel);
        });
}());​
As you can see, the selection and the sel references are pointing to the same Selection object so you don't need the sel variable and call the getSelection() method again.
The Selection object provides some methods to edit your selection but you can be more flexible on the Range objects inside the Selection. However, you will need 2 methods for sure, even if you are working with Range objects:
  • selection.removeAllRanges()
    Not surprisingly, this will remove all of the Range objects inside.
  • selection.addRange(range)
    Adds a Range object to the selection. Weird, huh? =)
A Selection may contain more than one Range, you can achieve that programmatically but as a user I have not found any need for that (yet...).

Range objects

Range objects contain the actual user-selection and it does not really matter if the selection is inside a contenteditable element or somewhere else, it is going to return the selection Range(s) anyway with the proper data about the selection.
I used mostly the range.setStart/setEnd(Node, position) method to set up my selection and place the caret back to the contenteditable when there was a change. These methods take a TextNode as a first argument and a position number.
Also, there are situations when you need a new Range object and throw out the existing one. These cases look something like this:
//the Selection object
var selection = window.getSelection();

//creates a new {Range} object
var newRange = document.createRange();

//this will place the caret at beginning of the 'textNode'
range.setStart(textNode, 0);

//clear up the selection object so Ranges won't collide
selection.removeAllRanges();

//add the new Range object
//this will handle the visul selection also
selection.addRange(newRange);
So these are the really basics of the selections. I think the best way of learning it is doing, so I will come back with examples about "how-to"s and "how-not-to"s soon. =)

Stay tuned, there is a lot more to this. Also, if everything goes as planned we will do some serious interaction going between a simple text editor and the user... ;)