Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
295 views
in Technique[技术] by (71.8m points)

iphone - apply style to range of text with javascript in uiwebview

I am displaying some simple styled text as html in a UIWebView on iPhone. It is basically a series of paragraphs with the occasional strong or emphasized phrase. At runtime I need to apply styles to ranges of text.

There are a few similar scenarios, one of which is highlighting search results. If the user has searched for "something" I would like to change the background color behind occurrences of the word, then later restore the original background.

Is it possible to apply styles to ranges of text using javascript? A key part of this is also being able to unset the styles.

There seem to be two likely paths to follow. One would be modifying some html in Objective-C and passing it through javascript as the new innerHTML of some container. The other would be to use javascript to directly manipulate DOM nodes.

I could manipulate html, but that sounds tedious in Objective-C so I would rather manipulate the DOM if that is a reasonable approach. I am not that familiar with javascript and DOM so I do not know if it is a reasonable approach.

I wrote some routines to translate between text ranges and node ranges with offsets. So if I start with text range 100-200 and that starts in one paragraph and ends in a third, I can get the text nodes and the offsets within the nodes that represent the given text range. I just need a way to split a text node at an offset in the text. Currently I just apply styles to the paragraphs containing the text range.

A few notes:

  • straight javascript please, no external frameworks like jquery.
  • the changes never need to be written to disk.
  • the changes should be undoable or at least removable.
  • the styles to apply already exist in a css file.
  • it needs to work in iPhone 3.0 and forward.
  • all the source files are shipped with the app.
  • please be verbose.

Thanks for any suggestions.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

I think you're asking a lot to get a complete solution for this, but it seemed interesting so I've implemented it. The following works in recent WebKit browsers, including Safari on iPhone running OS 3.0. It uses the non-standard but convenient intersectsNode method of Range, which exists in WebKit but was removed from Firefox in 3.0, so it doesn't work in recent versions of Firefox but could be made to do so trivially.

The following will surround each selected text node with a <span> element with a class of "someclass" and also a unique class to allow easy undoing. applyClassToSelection returns this unique class; pass this class into removeSpansWithClass to remove the spans.

UPDATE: Fixed problem when selection is entirely contained within a single text node

UPDATE 2: Now tested and works in iPhone running OS 3.0.

UPDATE 3: Added rangeIntersectsNode function to add support for Firefox 3.0 and later. This code should now work in Firefox 1.0+, Safari 3.1+, Google Chrome, Opera 9.6+ and possibly others (untested so far). It does not work at all in Internet Explorer and will give errors in that browser. I plan to work on an IE version soon.

<script type="text/javascript">
    var nextId = 0;

    var rangeIntersectsNode = (typeof window.Range != "undefined"
            && Range.prototype.intersectsNode) ?

        function(range, node) {
            return range.intersectsNode(node);
        } :

        function(range, node) {
            var nodeRange = node.ownerDocument.createRange();
            try {
                nodeRange.selectNode(node);
            } catch (e) {
                nodeRange.selectNodeContents(node);
            }

            return range.compareBoundaryPoints(Range.END_TO_START, nodeRange) == -1 &&
                range.compareBoundaryPoints(Range.START_TO_END, nodeRange) == 1;
        };

    function applyClassToSelection(cssClass) {
        var uniqueCssClass = "selection_" + (++nextId);
        var sel = window.getSelection();
        if (sel.rangeCount < 1) {
            return;
        }
        var range = sel.getRangeAt(0);
        var startNode = range.startContainer, endNode = range.endContainer;

        // Split the start and end container text nodes, if necessary
        if (endNode.nodeType == 3) {
            endNode.splitText(range.endOffset);
            range.setEnd(endNode, endNode.length);
        }

        if (startNode.nodeType == 3) {
            startNode = startNode.splitText(range.startOffset);
            range.setStart(startNode, 0);
        }

        // Create an array of all the text nodes in the selection
        // using a TreeWalker
        var containerElement = range.commonAncestorContainer;
        if (containerElement.nodeType != 1) {
            containerElement = containerElement.parentNode;
        }

        var treeWalker = document.createTreeWalker(
            containerElement,
            NodeFilter.SHOW_TEXT,
            // Note that Range.intersectsNode is non-standard but
            // implemented in WebKit
            function(node) {
                return rangeIntersectsNode(range, node) ?
                    NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
            },
            false
        );

        var selectedTextNodes = [];
        while (treeWalker.nextNode()) {
            selectedTextNodes.push(treeWalker.currentNode);
        }

        var textNode, span;

        // Place each text node within range inside a <span>
        // element with the desired class
        for (var i = 0, len = selectedTextNodes.length; i < len; ++i) {
            textNode = selectedTextNodes[i];
            span = document.createElement("span");
            span.className = cssClass + " " + uniqueCssClass;
            textNode.parentNode.insertBefore(span, textNode);
            span.appendChild(textNode);
        }

        return uniqueCssClass;
    }

    function removeSpansWithClass(cssClass) {
        var spans = document.body.getElementsByClassName(cssClass),
            span, parentNode;

        // Convert spans to an array to prevent live updating of
        // the list as we remove the spans
        spans = Array.prototype.slice.call(spans, 0);

        for (var i = 0, len = spans.length; i < len; ++i) {
            span = spans[i];
            parentNode = span.parentNode;
            parentNode.insertBefore(span.firstChild, span);
            parentNode.removeChild(span);

            // Glue any adjacent text nodes back together
            parentNode.normalize();
        }
    }

    var c;
</script>

<input type="button" onclick="c = applyClassToSelection('someclass')"
    value="Add class">
<input type="button" onclick="removeSpansWithClass(c)"
    value="Remove class">

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...