Search code examples
javascriptdomrangy

Rangy: word under caret (again)


I'm trying to create a typeahead code to add to a wysihtml5 rich text editor. Basically, I need to be able to insert People/hashtag references like Twitter/Github/Facebook... do.

I found some code of people trying to achieve the same kind of thing.

http://jsfiddle.net/A9z3D/

This works pretty fine except it only do suggestions for the last word and has some bugs. And I want a select box like Twitter, not a simple "selection switching" using the tab key.

For that I tried to detect the currently typed word.

        getCurrentlyTypedWord: function(e) {
            var iframe = this.$("iframe.wysihtml5-sandbox").get(0);
            var sel = rangy.getSelection(iframe);
            var word;
            if (sel.rangeCount > 0 && sel.isCollapsed) {
                console.debug("Rangy: ",sel);
                var initialCaretPositionRange = sel.getRangeAt(0);
                var rangeToExpand = initialCaretPositionRange.cloneRange();
                var newStartOffset = rangeToExpand.startOffset > 0 ? rangeToExpand.startOffset - 1 : 0;
                rangeToExpand.setStart(rangeToExpand.startContainer,newStartOffset);
                sel.setSingleRange(rangeToExpand);
                sel.expand("word", {
                    trim: true,
                    wordOptions: {
                         includeTrailingSpace: true,
                         //wordRegex: /([a-z0-9]+)*/gi
                         wordRegex: /[a-z0-9]+('[a-z0-9]+)*/gi
                        // wordRegex: /([a-z0-9]+)*/gi
                    }
                });
                word = sel.text();
                sel.removeAllRanges();
                sel.setSingleRange(initialCaretPositionRange);
            } else {
                word = "noRange";
            }
            console.debug("WORD=",word);
            return word;

This is only triggered when the selection is collapsed. Notice I had to handle a backward move of the start offset because if the caret is at the end of the word (like it is the case most of the time when an user is typing), then the expand function doesn't expand around the currently typed word.

This works pretty nicely until now, the problem is that it uses the alpha release of Rangy 1.3 which has the TextRangeModule. The matter is that I noticed wysihtml5 is also using Rangy in a different and incompatible version (1.2.2) (problem with rangy.dom that probably has been removed).

As Rangy uses a global window.rangy variable, I think I'll have to use version 1.2.2 anyway.

How can I do an equivalent of the expand function, using only rangy 1.2.2?

Edit: by the way, is there any other solution than using the expand function? I think it is a bit strange and hakish to modify the current selection and revert it back just to know which word is currently typed. Isn't there a solution that doesn't involve selecting the currently typed word? I mean just based on ranges once we know the initial caret collapsed range?


Solution

  • As Rangy uses a global window.rangy variable, I think I'll have to use version 1.2.2 anyway.

    Having read Rangy's code, I had the intuition that probably it would be feasible to load two versions of Rangy in the same page. I did a google search and found I was right. Tim Down (creator of Rangy) explained it in an issue report. He gave this example:

    <script type="text/javascript" src="/rangy-1.0.1/rangy-core.js"></script>
    <script type="text/javascript" src="/rangy-1.0.1/rangy-cssclassapplier.js"></script>
    
    <script type="text/javascript">
        var rangy1 = rangy;
    </script>
    
    <script type="text/javascript" src="/rangy-1.1.2/rangy-core.js"></script>
    <script type="text/javascript" src="/rangy-1.1.2/rangy-cssclassapplier.js"></script>
    

    So you could load the version of Rangy that your code wants. Rename it and use this name in your code, and then load what wysihtml5 wants and leave this version as rangy.

    Otherwise, having to implement expand yourself in a way that faithfully replicates what Rangy 1.3 does is not a simple matter.

    Here's an extremely primitive implementation of code that would expand selections to word boundaries. This code is going to be tripped by elements starting or ending within words.

    var word_sep = " ";
    
    function expand() {
        var sel = rangy.getSelection();
        var range = sel.getRangeAt(0);
    
        var start_node = range.startContainer;
        if (start_node.nodeType === Node.TEXT_NODE) {
            var sep_at = start_node.nodeValue.lastIndexOf(word_sep, range.startOffset);
            range.setStart(start_node, (sep_at !== -1) ? sep_at + 1 : 0);
        }
    
        var end_node = range.endContainer;
        if (end_node.nodeType === Node.TEXT_NODE) {
            var sep_at = end_node.nodeValue.indexOf(word_sep, range.endOffset);
            range.setEnd(end_node, (sep_at !== -1) ? sep_at : range.endContainer.nodeValue.length);
        }
        sel.setSingleRange(range);
    }
    

    Here's a fiddle for it. This should work in rangy 1.2.2. (It would even work without rangy.)