Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Any alternative to using execCommand for Highlighting HTML in iOS ebook reader

I'm developing a book reader for iOS using a UIWebView. At the moment I'm working with some basic HTML files but will eventually work with ePubs. I am looking for a suitable way of styling ranges of text. My ranges are a little bit special in that they usually encompass three ranges - a keyrange and a range immediately before and range immediately after. The keyrange may span multiple nodes and may start or end for example within a selection of bold text etc. The styling should not be written to file.

At the moment I have a working solution as follows:

document.designMode = "on";

// Color the first section
var selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range1);

if (!selection.isCollapsed){ 
document.execCommand("foreColor", false, color1);    
}

// Color the middle section
selection.removeAllRanges();
selection.addRange(range2);

if (!selection.isCollapsed){   
document.execCommand("backColor", false, color2);
document.execCommand("foreColor", false, color3);
}

// Color the last section
selection.removeAllRanges();
selection.addRange(range3);

if (!selection.isCollapsed){ 
document.execCommand("foreColor", false, color1);
}

document.designMode = "off";
selection.removeAllRanges();

This works fine but is noticeably slow (on an iPad2), even if I modify it to highlight a single range in a short HTML document. There is always a very noticeable delay before the text is stylised. Looking at ebook readers on iPad such as kindle or iBooks there is no discernible delay. How might they implement their highlighting function? Might they be reading the geographical location on the selected text and applying some kind of overlay?

I have searched for a better solution than I'm already using but with no luck so if anyone has an idea I would be very grateful.

Thank you!

like image 702
Simple99 Avatar asked Jan 18 '23 02:01

Simple99


1 Answers

I have made an alternative method to above which appears to be much faster. It is mainly based on mikeB's excellent code given in this question.

How to get nodes lying inside a range with javascript?

  1. I first split the start and end nodes creating new text nodes with only the text which falls with the given range. I adjust the range for the new nodes.

  2. I use a slightly modified version of the code at the above link to return an array of text nodes contained within the range.

  3. I loop through the array and put spans with styles around each text node.

As far as I've tested the code works well. When applying styles to a number of ranges simultaneously there is still a delay but its much better than before. I'd be interested to know if anyone has any improvements.

My code:

function styleRange(range, style) // the style is a string of css styles. eg."background-color:darkblue; color:blue;"
{    
    // Get the start and end nodes and split them to give new start and end nodes with only text that falls inside the range.
    var startNode = range.startContainer.splitText(range.startOffset);
    var endNode = range.endContainer.splitText(range.endOffset).previousSibling;

    // Adjust the range to contain the new start and end nodes
    // The offsets are not really important anymore but might as well set them correctly
    range.setStart(startNode,0);
    range.setEnd(endNode,endNode.length);

    // Get an array of all text nodes within the range
    var nodes = getNodesInRange(range);

    // Place span tags with style around each textnode
    for (i = 0; i < nodes.length; i++)
    {
        var span = document.createElement('span');
        span.setAttribute("style", style);
        span.appendChild( document.createTextNode(nodes[i].nodeValue));
        nodes[i].parentNode.replaceChild( span, nodes[i] );
    }
}

Modified code from mikeB's code:

function getNodesInRange(range)
{
    var start = range.startContainer;
    var end = range.endContainer;
    var commonAncestor = range.commonAncestorContainer;
    var nodes = [];
    var node;

    // walk parent nodes from start to common ancestor
    for (node = start.parentNode; node; node = node.parentNode)
    {
        if (node.nodeType == 3) //modified to only add text nodes to the array
            nodes.push(node);
        if (node == commonAncestor)
            break;
    }
    nodes.reverse();

    // walk children and siblings from start until end is found
    for (node = start; node; node = getNextNode(node))
    {
        if (node.nodeType == 3) //modified to only add text nodes to the array
            nodes.push(node);
        if (node == end)
            break;
    }

    return nodes;
}


function getNextNode(node, end)
{
    if (node.firstChild)
        return node.firstChild;
    while (node)
    {
        if (node.nextSibling)
            return node.nextSibling;
        node = node.parentNode;
    }
}
like image 62
Simple99 Avatar answered Jan 21 '23 07:01

Simple99