4

I've divided the Html content (which belongs to an eBook) into multiple columns using the following steps.

1) I've added the HTML inside the content which is inside a container.

<body>
<div id="container">
    <div id="content">

        BOOK HTML CONTENT

        <span id="endMarker"></span>
    </div>
</div>
</body>

2) Next, I've added the CSS style of the content and the container as shown below:

#container {
    width: 240px;
    overflow: hidden;
    background-color: yellow;
}
#content {
     position: relative;
     height: 30em;

    -moz-column-width: 240px;
    -webkit-column-width: 240px;
    column-width: 240px;

    -moz-column-gap: 10px;
    -webkit-column-gap: 10px;
    column-gap: 10px;
}

Now, I want to find the column number of the text (or a line) using javascript?

There are other questions on SO that show how to get the column number based on the id. In my case, there are no id's. The only thing available is the text (or line) and I need to get the column number by searching through the Html content.

Currently, I've two "solutions" to get the column number but they are incomplete.

1) I can find whether the text exists or not by using window.find(text) after that I'm not sure what I've to do.

2) Another option is to add <span> with an id to every line temporarily and remove it. Once added, I can get the total column count up to that line (like shown below).

columnCount = Math.floor($('#marker').position().left/(columnWidth + columnGap));

This will give a wrong number if the line is extended to another column.

The second solution is tricky and book content is huge. I don't think this is the best way to get the column number. I'm looking for a simpler solution.

halfpastfour.am
  • 5,764
  • 3
  • 44
  • 61
Srikar Reddy
  • 3,650
  • 4
  • 36
  • 58
  • I'm developing this for Android eBook app and any android developer that has worked on an eBook app might know the answer too along with javascript developers. Also, the javascript solution of yours will eventually gets called from android `webView.loadUrl()`. – Srikar Reddy May 23 '17 at 09:49
  • Please don't link to other questions in your own question. Questions should be posted to be self reliant, without external resources such as other questions. What if that question is deleted? Your question is broken. Paste relevant code you have in your own question so that people can understand what you're working with. – Glubus May 23 '17 at 09:51
  • @Glubus The link is supposed to give a backdrop on what I'm working on and how far I've reached. Users can provide solutions without going through the link. I don't think this question is that much reliant on the question shared. Also, I'm not sure what code to share. It just a regular HTML file with head, body tag and few lines of javascript that splits single page into multiple columns. There is nothing more in it. If you want a specific part of the code that you feel is relevant and needs to be added let me know. – Srikar Reddy May 23 '17 at 10:09
  • @SrikarReddy There is little to no way for us to show you how to do what you want without knowing what your code looks like. if the question you linked is only similar to your own code, then your own code is way more relevant than the shared question, meaning you need to at least add your own code, and might as well remove the shared question to avoid confusion. – Glubus May 23 '17 at 10:12
  • Okay so now what data do you want? You mention you want to select on text, but you don't provide detail on what you want to get specifically. – Glubus May 23 '17 at 11:05
  • @Glubus I want the Column number. Remember the book, I was talking about. it has chapter and those chapters contain paragraphs which then has lines. I want a javascript function that takes any line as a parameter and returns the column number in which it is in. – Srikar Reddy May 23 '17 at 11:36
  • I see now, and would your solution return a list of columns if the text to be searched exceeds a single column, or is the search requirement that is has to be a text within 1 column? – Glubus May 23 '17 at 12:45
  • @Glubus Sometimes the text exceeds to another column even then it's fine if the javascript function returns the first column number the text was found in. Eg: let say if a text was found to start in column 5 and it extended to column 6 then javascript function can return 5. It doesn't necessarily have to return 6. Finally, the (or any) text won't exceed more than two columns. – Srikar Reddy May 24 '17 at 04:58

2 Answers2

3

Try this, made a workable version for your question. jsfiddle link

Although OP didn't tag question with jQuery, but actually used jQuery inside the question, I use it too for cleaner code. (and fit the question)

What I do in this example:

  1. Make a long content cross several pages to visualize paging (with css: column-width).
  2. Click on previous / next to browse pages.
  3. Input and click 'find' button, make found texts highlighted.
  4. List all columns (pages) found with input text.
  5. Click on the link and jump to column with searched text.

In detail I made temporary DOM elements to calculate column, and remove them right after to keep DOM tree clean.

2017/6/1 Edited: Added highlight color for searched text.

  $('#find').click( () => {
    var text = $('#findtext').val();
    var columns = [];

    var doms = [];
    while (window.find(text, false)) {
      var sel = window.getSelection();
      if (sel) {
        var range = sel.getRangeAt(0);

        var el = document.createElement("span");
        var frag = document.createDocumentFragment();
        frag.appendChild(el);
        range.insertNode(frag);

        columns.push(Math.floor(el.offsetLeft/(_columnWidth + _columnGap)));
        doms.push(el);
      }
    }

    /// distinct
    columns = columns.filter( (value, index, self) => self.indexOf(value) === index );

    /// show result    
    $("#foundlist").empty();
    for (var i=0; i<columns.length; i++)
      $("#foundlist").append(`<li><a href="#" onclick="setColumn(${columns[i]})">Found in Column ${columns[i]+1}</a></li>`);

    /// remove dom. keep dom tree clean
    while (doms.length > 0) {
      var dom = doms.pop();
      dom.parentNode.removeChild(dom);
    }
  });
Val
  • 21,938
  • 10
  • 68
  • 86
  • Your solution is working if I comment out this line `if (first) { first = false; continue; }` – Srikar Reddy Jun 01 '17 at 05:39
  • Can you tell me what is the purpose of the boolean `first`? – Srikar Reddy Jun 01 '17 at 05:40
  • @SrikarReddy because that input box I put on top will match the first 'window.find'. I'll add comment to jsfiddle. you can remove it freely =) – Val Jun 01 '17 at 05:42
  • Thank you. The reason that line didn't work for me mainly because there is no input box when I integrated your code and I've turned your click function into a normal function. – Srikar Reddy Jun 01 '17 at 05:54
  • 1
    @SrikarReddy you're welcome. leave message to me if you need any further help. – Val Jun 01 '17 at 05:58
0

Instead of adding a <span> beforehand, you could temporarily insert it at the point where you find your text and remove it again as soon as you have identified the position.

The key is how to find text in a long document. The interface for this task is the TreeWalker, that can iterate through every text node in a DOM subtree.

How to insert an element in the middle of a text node was copied from here.

Your question did not state it, but used jQuery as a dependency. This solution is using only the vanilla DOM interfaces.

var columnWidth = 240,
    columnGap   = 10;

function getColumn(text) {
  // the subtree to search in
  var root = document.getElementById('content');
  // define an iterator that only searches in text nodes
  var treeWalker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
    // filter the text nodes to those containing the search text
    acceptNode: function(node) {
      if ( node.data.includes(text) ) {
        return NodeFilter.FILTER_ACCEPT;
      }
    }
  });

  // look if there is a result
  if (treeWalker.nextNode()) {
    var node = treeWalker.currentNode;

    // get start index of found text
    var index = node.data.indexOf(text);
    // and split into two nodes, referencing the second part
    var foundTextNode = node.splitText(index);

    // define an empty inline element
    var span = document.createElement('span');
    // insert it between the two text nodes
    var elem = node.parentElement;
    elem.insertBefore(span, foundTextNode);

    // compute the column from the position of the element
    // you might have to account for margins here
    var x = span.getBoundingClientRect().left - root.getBoundingClientRect().left;
    var column = Math.floor(x / (columnWidth + columnGap));

    // restore previous state
    elem.removeChild(span);
    elem.normalize();

    return column;
  } else {
    // no result
    return false;
  }
}

The obvious limitation to this solution is that it will not find text that is spanning multiple nodes. So, if you have a text fragment

<p>The quick brown fox <em>jumps</em> over the lazy dog.</p>

a search for 'fox jumps over' will not find this sentence.

ccprog
  • 20,308
  • 4
  • 27
  • 44