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.
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">