Search code examples
javascripttextselectionrangy

cssClassApplier only on selected text in specified divs


I'm trying to add classes to selected text in a document. The problem is that I don't want the user to be able to select ALL the text on the page (using command+A for example...) So I would like to remove nodes from a range but I don't know how to do that. This page has a lot of text / nested divs so using the added class for checking if it is in an element that is selectable would be very time consuming / slow.

I made a fiddle to explain what I'm trying to do a bit better. http://jsfiddle.net/thomasjonas/BhKFt/20/

If you select all the text in this example the class is added to all the divs. I want it to only apply the css to the divs with the 'yes' class. Is there a nice, non-browser-crashing solution for this?

Thanks in advance!


Solution

  • The following is a little heavy-handed and inefficient because it checks each element with class "yes" intersects the selection. You could improve it by checking in advance whether the selection lies completely within a single element with the "yes" class, for example. It uses Rangy's proprietary intersection() method of range objects.

    Demo: http://jsfiddle.net/timdown/BhKFt/23/

    Code:

    // getElementsByClassName implementation for browsers without it
    // (IE <= 7, for example)
    var getElementsByClassName =
        (typeof document.documentElement.getElementsByClassName != "undefined") ?
    
        function(el, cssClass) {
            return el.getElementsByClassName(cssClass);
        } :
        function(el, cssClass) {
            var allEls = el.getElementsByTagName("*");
            var elsWithClass = [];
            var classRegex = new RegExp("(?:^|\\s)" + cssClass + "(?:\\s|$)");
            for (var i = 0, len = allEls.length, el; i < len; ++i) {
                el = allEls[i];
                if (el.className && classRegex.test(el.className)) {
                    elsWithClass.push(el);
                }
            }
            return elsWithClass;
        };
    
    $(document).ready(function(){
        rangy.init();
        $(document).mouseup(function(){
            var sel = rangy.getSelection();
            var range = sel.getRangeAt(0);
            var classApplier = rangy.createCssClassApplier("tmp");
    
            var els = getElementsByClassName(document.body, "yes");
    
            // Create an array of ranges that represent the intersection of
            // the selection with each "yes" element
            var rangesWithClass = [];
            for (var i = 0, len = els.length, elRange; i < len; ++i) {
                if (range.intersectsNode(els[i])) {
                    elRange = rangy.createRange();
                    elRange.selectNode(els[i]);
                    rangesWithClass.push(range.intersection(elRange));
                    elRange.detach();
                }
            }
    
            // Apply the class to the ranges obtained in the last step
            for (i = 0, len = rangesWithClass.length; i < len; ++i) {
                classApplier.applyToRange(rangesWithClass[i]);
                rangesWithClass[i].detach();
            }
    
            sel.removeAllRanges();
        });
    });
    

    It may be useful if there was some kind of filtering option to the options object passed into rangy.createCssClassApplier(). I'll have a think.