最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Get point when user stopped reading - Stack Overflow

programmeradmin3浏览0评论

User A gets on my online magazine. She starts reading a very long article, then in the middle of it she decides to pause. She closes the browser and only es back to the site many hours later. Now when she opens again the same article I would like the page to start from where she had left it.

Imaging User A is actually a registered user, how can I achieve this? More specifically, I would like to store the character number (as an int) of the first character of the first visible line on the screen (basically the top-left corner).

Edit: Just to clarify even further, you can think of apps like Instapaper or Pocket to be facing a similar issue (both the mobile and web apps).

User A gets on my online magazine. She starts reading a very long article, then in the middle of it she decides to pause. She closes the browser and only es back to the site many hours later. Now when she opens again the same article I would like the page to start from where she had left it.

Imaging User A is actually a registered user, how can I achieve this? More specifically, I would like to store the character number (as an int) of the first character of the first visible line on the screen (basically the top-left corner).

Edit: Just to clarify even further, you can think of apps like Instapaper or Pocket to be facing a similar issue (both the mobile and web apps).

Share Improve this question edited Jun 16, 2018 at 13:38 Edgar Derby asked Jun 13, 2018 at 7:46 Edgar DerbyEdgar Derby 2,8256 gold badges32 silver badges56 bronze badges 8
  • 1 What have you tried so far? – Djave Commented Jun 13, 2018 at 7:47
  • @nickzoum: localStorage would probably be better than a cookie. Cookies are meant for serverside use, localStorage is a better choice for clientside-only stuff... – Amadan Commented Jun 13, 2018 at 7:49
  • @nickzoum I didn't propose to store the character but the character's position. Image the article is 10k characters long, the top left character could be the 3276th character. – Edgar Derby Commented Jun 14, 2018 at 8:35
  • @EdgardDerby Updated my answer, I know its not exactly what you asked for but I hope it helps – nick zoum Commented Jun 22, 2018 at 9:13
  • 1 There won't be any option to get the exact character in top-left corner of viewport. Best you could get is the top-most element which is visible. To get closer you could calculate the position of that element. If this is enough (and your text is well structured into paragraphs) this will be more accurate as @nickzoum's answer but also not as accurate as you asked for. – jelhan Commented Jun 22, 2018 at 20:33
 |  Show 3 more ments

5 Answers 5

Reset to default 10

Updated Answer

I am not really sure how that can be done. I would remend storing how much the document has been scrolled using localStorage.

You can get how far the document was scrolled and the dimensions of the document at the time it was closed, If the width of the document is the same when it gets reloaded then you can just scroll to the same point. If not you can estimate the point where the user left off by calculating the volume read width * scrollTop and dividing that by the current width.

Here is a pseudo-code to help you understand the concept:

if oldWidth == currentWidth
    set scroll = oldScroll
else
    set scroll = oldScroll * oldWidth / currentWidth

Here is a JavaScript example of the above pseudo-code, but some notes first:

  • localStorage is not allowed on snippets so the current code will not work here, if you want to test you will have to copy it elsewhere
  • The leavePage() method has a return null statement at the end, this is because some browsers will not run a function without a return statement on beforeunload

if (document.readyState === "plete") enterPage();
else addEventListener("load", enterPage);
addEventListener("beforeunload", leavePage);

function enterPage() {
  var magazineDom = document.querySelector("#magazine");
  if (magazineDom instanceof HTMLElement) {
    magazineDom.textContent = "Lorem Ipsum".repeat(800);
    //onRender();
  }
}

function onRender() {
  var magazineContainerDom = document.querySelector("#magazine-container");
  var magazineDom = document.querySelector("#magazine");
  if (magazineContainerDom instanceof HTMLElement && magazineDom instanceof HTMLElement) {
    var info = getMagazineInfo(magazineDom.getAttribute("magazine"));
    if (info) {
      var currentWidth = magazineDom.scrollWidth;
      if (currentWidth == info.width) magazineContainerDom.scrollTop = info.top;
      else magazineContainerDom.scrollTop = info.top * info.width / currentWidth;
    }
  }
}

function leavePage() {
  var magazineContainerDom = document.querySelector("#magazine-container");
  var magazineDom = document.querySelector("#magazine");
  if (magazineContainerDom instanceof HTMLElement && magazineDom instanceof HTMLElement) {
    setMagazineInfo(magazineDom.getAttribute("magazine"), magazineContainerDom.scrollTop, magazineDom.scrollWidth);
  }
  return null;
}

function getMagazineInfo(name) {
  return JSON.parse(localStorage.magazines || "{}")[name];
}

function setMagazineInfo(name, top, width) {
  var magazines = JSON.parse(localStorage.magazines || "{}");
  magazines[name] = {
    top: top,
    width: width
  };
  localStorage.magazines = JSON.stringify(magazines);
}
body {
  position: fixed;
  bottom: 0;
  right: 0;
  left: 0;
  top: 0;
}

#magazine-container {
  overflow-y: auto;
  height: 100%;
  width: 100%;
}

#magazine {
  max-width: 500px;
  margin: auto;
}
<body>
  <div id="magazine-container">
    <div id="magazine" magazine="First Document">

    </div>
  </div>
</body>

I had some fun with this. The script is creating an array of lines. The array may be searched for a term, that is stored in a Cookie or better the local storage.

See

function findLine4(str)

Hope you find some ideas for your issue below. Please don't mind, its only some dirty script yet.

<html>
<body>
    <div id="containerWrapper"></div>
    <div id="testWrapper"></div>

    <div id="originDiv" style="background:#fee;">
    Lorem ipsum dolor sit amet, consectetuer adipiscing 
elit. Aenean modo ligula eget dolor. Aenean massa 
<strong>strong</strong>. Cum sociis natoque penatibus 
et magnis dis parturient montes, nascetur ridiculus 
mus. Donec quam felis, ultricies nec, pellentesque 
eu, pretium quis, sem. Nulla consequat massa quis 
enim. Donec pede justo, fringilla vel, aliquet nec, 
vulputate eget, arcu. In enim justo, rhoncus ut, 
imperdiet a, venenatis vitae, justo. Nullam dictum 
felis eu pede <a class="external ext" href="#">link</a> 
mollis pretium. Integer tincidunt. Cras dapibus. 
Vivamus elementum semper nisi. Aenean vulputate 
eleifend tellus. Aenean leo ligula, porttitor eu, 
consequat vitae, eleifend ac, enim. Aliquam lorem ante, 
dapibus in, viverra quis, feugiat a, tellus. Phasellus 
viverra nulla ut metus varius laoreet. Quisque rutrum. 
Aenean imperdiet. Etiam ultricies nisi vel augue. 
Curabitur ullamcorper ultricies nisi.
<br /><br /><br />
    Lorem ipsum dolor sit amet, consectetuer adipiscing 
elit. Aenean modo ligula eget dolor. Aenean massa 
<strong>strong</strong>. Cum sociis natoque penatibus 
et magnis dis parturient montes, nascetur ridiculus 
mus. Donec quam felis, ultricies nec, pellentesque 
eu, pretium quis, sem. Nulla consequat massa quis 
enim. Donec pede justo, fringilla vel, aliquet nec, 
vulputate eget, arcu. In enim justo, rhoncus ut, 
imperdiet a, venenatis vitae, justo. Nullam dictum 
felis eu pede <a class="external ext" href="#">link</a> 
mollis pretium. Integer tincidunt. Cras dapibus. 
Vivamus elementum semper nisi. Aenean vulputate 
eleifend tellus. Aenean leo ligula, porttitor eu, 
consequat vitae, eleifend ac, enim. Aliquam lorem ante, 
dapibus in, viverra quis, feugiat a, tellus. Phasellus 
viverra nulla ut metus varius laoreet. Quisque rutrum. 
Aenean imperdiet. Etiam ultricies nisi vel augue. 
Curabitur ullamcorper ultricies nisi.
    </div>

</body>
<script>
    (function init() {
        var i;
        var originDiv = document.getElementById("originDiv");
        var allowedWidth = originDiv.scrollWidth;

        function testBreak(testDiv, strTest) {
            testDiv.innerHTML = strTest + "..."; // Yepp, that's the point. No clue, how to fix this.
            return testDiv.scrollWidth > allowedWidth;
        }

        function tokenizeText() {
            return originDiv.innerHTML.trim()
                .split(/([\s]*<[a-zA-Z]+.*>.*<\/.*>[.,;:-]*[\s]*)|[\s]+/ig)
                .filter(function(a){return typeof(a) != 'undefined'})
                .map(function(a){return a.trim()}); 
        }

        function createLineArray() {
            var testDiv = originDiv.cloneNode(true);
            testDiv.innerHTML = "";
            testDiv.setAttribute("style", "white-space:nowrap;background:#ccc;visibility:hidden;");
            document.getElementById("testWrapper").appendChild(testDiv);

            var htmlWords = tokenizeText();
            var lines = [];
            var currLine = '';
            for(i = 0; i < htmlWords.length; i++) {
                currLine += htmlWords[i] + ' ';
                if(testBreak(testDiv, currLine)) {
                    lines.push(currLine.substr(0, currLine.length - htmlWords[i].length - 1));
                    currLine = '';
                    i--;
                }
            }
            lines.push(currLine);       
            return lines;
        }

        function createNewDiv(html) {
            var newDiv = document.createElement("DIV");
            newDiv.setAttribute("style", "background:#ddf");
            newDiv.setAttribute("id", "newContainer");  
            newDiv.innerHTML = html;
            return newDiv;  
        }

        (function doInit() {
            var lines = createLineArray();
            var formattedText = '';
            for (i = 0; i < lines.length; i++) {
                formattedText += '<div class="line'+i+'">' + lines[i] + '</div>';
            }           
            document.getElementById("containerWrapper").appendChild(createNewDiv(formattedText));
            // originDiv.style.display="none";
        })();

        (function findLine4(str) {
            //TODO: tokenize str and search for matching tokens.
            var nodes = document.getElementById("newContainer").childNodes;
            for(i = 0; i < nodes.length; i++) {
                if(nodes[i].textContent.indexOf(str) != -1) {
                    setTimeout(function() {
                        window.scrollTo(0, nodes[i].offsetTop);
                    },1);
                    break; 
                }
            }
        })("elementum semper");

        /* 
            I personally would prefer a timer instead of a listener here. 
        */
        window.addEventListener("scroll", function(evt) {
            var sct = window.scrollY;
            var nodes = document.getElementById("newContainer").childNodes;
            for(var i = 0; i < nodes.length; i++) {
                if(nodes[i].offsetTop > sct) {
                    console.log("line " + i + ": " + nodes[i].innerHTML);
                    break;
                }
            }
        }, true);       

    })();


</script>
</html>

You can just store number of fullstops passed by, as in, number of lines read actually. And next time, when user opens the webpage again, you can skip all those full stops (lines) and pick next line and move scroll to that line of the plete text.

Or if you want it to be more specific, then you can store bination of number of fullstops and number of words read with localStorage key like ("lastread", "f20w5") for 5th word after 20th line.

Here is a simple solution precisely for what was requested: storing and returning first visible character of a text being read. In this solution, the user moves the characters (or words) with the scroll wheel only, but of course there could be any other inputs as well (e.g. jump larger sections with arrows or swiping on phone, or whatever).

See: https://jsfiddle/gasparl/re62ufy3/99/

HTML:

<div id="text_div"></div>

CSS:

#text_div {
    color: #ffffff;
    width: 400px;
    height: 250px;
    overflow:hidden;
    background-color: #000000;
}

JavaScript:

var scroll_speed = 100 // how many characters to move per scroll
var last_scroll_position = localStorage.scroll_position || 0; // get previous position if any
var the_text =
    "Lorem ipsum dolor sit amet, pellentesque libero metus quis sed et vestibulum, ut eget, turpis sed consequat, possimus quis, mi nec leo at wisi interdum posuere. Eu dui vitae, quis habitasse arcu arcu neque, odio enim.<br><br>Amet luctus lectus morbi massa nec, metus cras in molestie dolor, congue sodales viverra odio libero neque. Egestas vulputate vitae libero sodales, pharetra nec ligula vitae, tellus viverra ac vel, amet in sit eget.<br><br>Sit morbi tortor dictum viverra, massa molestie est urna suscipit felis massa, mi dictumst magna blandit, fringilla ut arcu nam urna ut in, et eget mauris metus integer ante.<br><br>Natoque egestas donec, ligula nam sit, ipsum a id, pellentesque in, modo est eu phasellus sem et. Ut leo elit bibendum, posuere cras et nec, fames in wisi porta ac eleifend. Odio iaculis arcu, dapibus eget in imperdiet, dolor eleifend bibendum eget wisi quis, ultrices metus et lacinia integer. At pellentesque quam sapien, duis faucibus nisl eu dapibus, amet venenatis dictumst quis dictum.<br><br>Adipiscing gravida, massa mi ac, suscipit porta consequat pretium lobortis lacus, nullam facilisis ac gravida habitant, sed faucibus et at porta aliquet.";
document.getElementById("text_div").innerHTML = the_text.substring(localStorage.start_position || 0); // set text to previous position or to 0 (very beginning)
window.addEventListener("wheel", function(e) {
    if (e.deltaY < 0 && last_scroll_position > 0) { // move text upwards, unless already at the very beginning
        last_scroll_position--;
    }
    if (e.deltaY > 0 && last_scroll_position * scroll_speed < the_text.length ) { // move text downwards, unless reached end of text
        last_scroll_position++;
    }
    var text_start = last_scroll_position * scroll_speed; // speed up scrolling
    if (text_start != 0) { // unless the position is at the beginning, look for the first uping space (or the end of text) to show only entire words
        while (
            the_text.length > text_start &&
            the_text.charAt(text_start) != " "
        ) {
            text_start++;
        }
    }
    document.getElementById("text_div").innerHTML = the_text.substring(text_start); // set text according to position
    localStorage.scroll_position = last_scroll_position; // store scrolling position
    localStorage.start_position = text_start; // store starting character position
});

Note that I store both scrolling position and starting character position - of course, either could be calculated from the other, but I think the simplest and most straightforward way is to just store both.

This seems easy at first, but can get really difficult depending on your sites content. We need to figure out how to save the progress and measure progress.

Storing Progress

For storing progress information while the user is reading the article. When the user visits the page some time later, you can detect the user is revisiting after a long pause by checking the last time the user's progress has been recorded. I would store the progress as a map where the key is the article id, and the value is an object containing progress, and date. Progress can be stored as percent with 0.0 as start and 1.0 as end. I suggest using universal Date::getTime in javascript for recording time.

{
  "1234567": { progress: 0.5, date: 1529733009679 }
}

Measuring Progress

This depends on the content. I made a list of things to consider when recording and returning user where they left off.

  • Content can change over time
  • Different font character widths can vary depending on browser width and line breaks and could cause inaccurate returns. This is because most applications have different logic when deciding the break a paragraph of words into multiple lines.
  • Different sized ads can change depending on config and inventory
  • Responsive sites could change layout of content as well as show or hide content
  • Types of content
    • text with paragraphs
    • paragraphs with images
    • carousels containing content
    • videos

You get the point. This list could get huge, and may not be easy to maintain for some sites. Simply recording the scroll position is not an option for layouts containing text, carousels or videos. We need to think about what all content has in mon. It appears that all content can be chunked into pieces, like paragraphs for text, or scenes for videos, etc. We can call each piece a checkpoint. I suggest finding a deterministic way to generate progress checkpoints within content, and record the last seen checkpoint periodically.

Example

For an article containing paragraphs and images, I would mark each paragraph and image as a checkpoint. Then simply record the checkpoint the user is currently viewing. We can determine the last seen checkpoint by using window.scrollY with HTMLElement.scrollTop for browser apps. This will only work if the position of each checkpoint in the content is the same for all layouts. This method works for all designs and all types of content. The logic can vary depending on your content, so I suggest abstracting this functionality for re-usability as this method will work for all types of content. Hope this helps :)

发布评论

评论列表(0)

  1. 暂无评论