Friday, September 1, 2006

DOM frustrations

My first tech post.  Here we go.
I spent this morning trying to add a feature to a web app I'm working on, with no success.  Here's what I'm trying to do: I have a form with a textarea where the user can type plain text that will later be interpreted by a preprocessor for a special trick and then treated as Markdown to produce HTML.
The special trick is that you can put a number reference--for example, [123]--into the text using a special add function that lets you look up the object you want to refer to, and then my preprocessor will turn it into a link to a document on our intranet.  (One reason for this is that if the document's location changes--which it easily can due to our documentation system setup--whenever the preprocessor serves up the page, all of the links will still be correct, because the preprocessor looks up the reference in a table.)
The database backend for this is down pat.  The feature I'm trying to implement on the client-side is to treat any block inserted with the special add function as an atomic unit, like a single character.  That is, you shouldn't be able to put the text cursor to the right of the '[' or to the left of the ']' in any bona fide number reference, nor should they be able to select or delete /part/ of the block: it should always be all or nothing.
(I know, there would be issues with pasting text containing a bona fide block, where it would have to use AJAX to verify anything looking like a reference if the user pastes text.  I'm okay with that, if I could get past these other hurdles first.)
I'm coding for Firefox only at this point.
The problem is, the DOM, AFAICT, provides no way to find out a) where the keyboard cursor currently is, nor b) what the current text selection is.  IE and Firefox both did things their own way, as sometimes happens.  The Firefox way is to provide textarea.selectionStart and textarea.selectionEnd, as read/write properties.  Super! Should be straightforward to do what I want to do, right?
Not exactly.  As it turns out, impossible, at least as far as I can see.  If you have a solution, I would love to hear it, because the basic functionality crap I'm going to have to fall back on is sub-par.  Here's why:  If you're selecting text from left to right, everything is fine: start stays where it is, and end moves left or right depending on whether you are decreasing or increasing the selection.  From right to left, end stays where it is, and start moves left or right, vice versa from before. Okay, so I just ensure the function examines both start and end and bumps them if either of them is in a block.  After all, the user could make an arbitrary selection with the mouse that begins in the middle of one block and ends in the middle of another.
So I did that.  It works, selection-wise.  The thing is, the cursor itself *always* jumps to the end point when the end point is set.  This is a problem because if you start at point B and select backwards--which is something I often do, if it's handy to do so based on where the cursor is already when I decide to select something--and you hit a block, it jumps the *beginning* of the selection over the block, but now all of the sudden your cursor is at the *end*.  (That is, it's impossible to manufacture a reverse selection.)  If you're holding down shift and left arrow to do this, a moment ago you were expanding your selection to the left, and now the left end (start) is stopped dead, and you're suddenly shrinking the selection from the right.  Gaah!
How can this be?  The reason is, as implied two paragraphs previous, a selection from A to B is stored the same way as one from B to A.  This was a smart choice in that it maintains consistency: "length" properties are always positive.  However, the *only* way to tell the difference is to know where the cursor is, which, even with Firefox's extension to the DOM, is not something you can do from JavaScript.  Google for it and you'll find a million pages telling you how to find the cursor, but they're all assuming there's no selection, so they're just looking at selectionStart and selectionEnd.  Useless!
Not quite, you might say.  We can keep track of previous starts and ends and thus know which end of the selection has moved.  Okay, so I do that.  Now have a look at the event model.  Bubbling or capturing, either way, the selection doesn't change until *after* the keydown and keypress events fire, so looking at the selection properties at that point is useless--you're always a step behind the user!  Okay, so we'll use keyup, which works correctly.  Brilliant, except that if the user is holding a key down, they don't get the visual jump outside the block, because block checking only happens when they let go.
If you think you could just take into account the arrow key direction to know where the cursor will end up if you're using the keydown or keypress events, consider trying to predict its location if you press the up or down arrows.  Welcome to the world of calculations dependent on font size and trying to match the browser's rendering engine...
And anyway, that still doesn't solve the earlier part about the cursor always being at the end.
Golly.  All for the lack of being able to get and set the cursor position!  Maybe I should've just bitten the bullet and written my own textarea.  Some day I suppose that's what I'll have to do.

No comments:

Post a Comment