19

I have a difficult situation with html and javascript. My html page allows user to select text and highlight it with colors. Now I want to save the state into database to show it later for that user. Of course, I can save whole html after user edited it. But I just ONLY want to save some parameters, combine with original html to show the page in the state user see last time. We can use this function:

var index = innerHTML.indexOf(text);

to highlight text at that index. But in case there are many same texts in the page, I want to highlight exactly word user highlighted it before.

Anyone can instruct me how to accomplish this with javascript?

I appreciate your help a lot.

beaver
  • 17,333
  • 2
  • 40
  • 66
user2804659
  • 191
  • 1
  • 3
  • 1
    Why don't you save the indices of the highlighted word, and their lengths in an array? – mohammed essam Sep 22 '13 at 17:26
  • I agree with @mohammedessam, create an array so that a user highlighting 'xyz' will add 'xyz' to an array and also would include an index to indicate which specific instance of 'xyz' it was (i.e. if it was the 3rd instance of 'xyz' the then the index would be 3. – tamak Sep 22 '13 at 17:35
  • I doubt `innerHTML.indexOf(text)` will deliver anything, let alone an index. A `ReferenceError` is more likely. – KooiInc Sep 22 '13 at 17:36
  • Adding some reference of how you're achieving the highlighting would be appreciated, as we should actually modify the code of the highlighting function to include the index of the text with it, so that you could actually know which instance of the text you should actually highlight, instead of knowing the highlighted text itself, which would be useless alone! – mohammed essam Sep 22 '13 at 18:00
  • you can use the following to count the number of instances of a substring, maybe somehow you can use it to arrive at the answer of detecting WHICH one is highlighted then use this to iterate through them and maybe count the total number of characters from start to 'find' where exactly to start the highlighting and based on length of string, know where to end the highlighting? var count = $("#textblock").html().match(/YOURSTRING/g); alert(count.length); – tamak Sep 22 '13 at 18:05
  • Can we accomplish this using jQuery ? – user2804659 Sep 24 '13 at 04:26
  • somewhat related to this question. I developed a [corpus annotation utility](https://github.com/beyond2013/dynamicContent) in [shiny](https://shiny.rstudio.com/) – Imran Ali Jan 14 '18 at 13:12

8 Answers8

12

Range objects and document.execCommand allow to manipulate selection pretty easily. The main problem in your case is saving the range object in a text format.

Basically what you need is to get the startContainer, startOffset, endContainer and endOffset, which are the values needed to create Range objects. Offsets are number so it's pretty straightforward. Containers are Nodes, which you can't directly save as strings, so that's the main problem. One thing you can do is add keys to your DOM and save the key. But then, since in ranges containers are text nodes, you'll need to save the index of the text node. Something like this should allow to tag the DOM with keys, using a recursive function:

function addKey(element) {
  if (element.children.length > 0) {
    Array.prototype.forEach.call(element.children, function(each, i) {
      each.dataset.key = key++;
      addKey(each)
    });
  }
};

addKey(document.body);

Once this is done, you can convert range objects to an object that you can save as a string. Like this:

function rangeToObj(range) {
  return {
    startKey: range.startContainer.parentNode.dataset.key,
    startTextIndex: Array.prototype.indexOf.call(range.startContainer.parentNode.childNodes, range.startContainer),
    endKey: range.endContainer.parentNode.dataset.key,
    endTextIndex: Array.prototype.indexOf.call(range.endContainer.parentNode.childNodes, range.endContainer),
    startOffset: range.startOffset,
    endOffset: range.endOffset
  }
}

Using this, you can save each selection that the user creates to an array. Like this:

document.getElementById('textToSelect').addEventListener('mouseup', function(e) {
  if (confirm('highlight?')) {
    var range = document.getSelection().getRangeAt(0);
    selectArray.push(rangeToObj(range));
    document.execCommand('hiliteColor', false, 'yellow')
  }
});

To save the highlights, you save each object to JSON. To test this, you can just get the JSON string from your range objects array. Like this (this is using the get Seletion button at the top):

document.getElementById('getSelectionString').addEventListener('click', function() {
  alert('Copy string to save selections: ' + JSON.stringify(selectArray));
});

Then when loading the empty HTML, you can use a reverse function that will create ranges from the objects you saved in JSON. Like this:

function objToRange(rangeStr) {
  range = document.createRange();
  range.setStart(document.querySelector('[data-key="' + rangeStr.startKey + '"]').childNodes[rangeStr.startTextIndex], rangeStr.startOffset);
  range.setEnd(document.querySelector('[data-key="' + rangeStr.endKey + '"]').childNodes[rangeStr.endTextIndex], rangeStr.endOffset);
  return range;
}

So you could have an array of ranges in strings that you convert to objects, and then convert to Range objects that you can add. Then using execCommand, you set some formatting. Like this (this is using the set selection button at the top, you do this after refreshing the fiddle):

document.getElementById('setSelection').addEventListener('click', function() {
  var selStr = prompt('Paste string');
  var selArr = JSON.parse(selStr);
  var sel = getSelection();
  selArr.forEach(function(each) {
    sel.removeAllRanges();
    sel.addRange(objToRange(each));
    document.execCommand('hiliteColor', false, 'yellow')
  })
});

See: https://jsfiddle.net/sek4tr2f/3/

Note that there are cases where this won't work, main problematic case is when user selects content in already highlighted content. These cases can be handled, but you'll need more conditions.

Julien Grégoire
  • 16,864
  • 4
  • 32
  • 57
3

You need to capture the path of the node in order to know its location.
This can be done in several ways.
The easiest way is to traverse the dom up until the body and create a selector.

function getPathFromElement(element) {
    var stack = [];

    while (element.parentNode != document.documentElement) {
        var sibCount = 0;
        var sibIndex = 0;
        var childNodes = element.parentNode.childNodes;
        var childLength = childNodes.length;

        for (var i = 0; i < childLength; i++) {
            var sib = childNodes[i];

            if (sib.nodeName == element.nodeName) {
                if (sib === element) {
                    sibIndex = sibCount;
                }

                sibCount++;
            }
        }

        if (element.hasAttribute("id") && element.id !== "") {
            stack.unshift(`${element.nodeName.toLowerCase()}#${element.id}`);
        }
        else if (sibCount > 1) {
            stack.unshift(`${element.nodeName.toLowerCase()}:eq(${sibIndex})`);
        }
        else {
            stack.unshift(element.nodeName.toLowerCase());
        }

        element = element.parentNode;
    }

    return stack.join(" > ")
}

Lets assume that you want to give your users two options to select text.

  1. Simple text in the page.
  2. Selecting text inside input text or textarea.

For the first option you can use a button with click handler or mouseup event.
I will use a button for simplicity.

function sendDataToServer(data) {
}

document.querySelector("#button").addEventListener("click", function (e) {
    var { target, text } = getSelectionTextAndContainerElement();

    var path = getPathFromElement(target);

    sendDataToServer({
        path: path,
        text: text 
    }); 
});

getSelectionTextAndContainerElement function basicaly selects the text and the element.

function getSelectionTextAndContainerElement() {
    var text;
    var containerElement = null;

    if (typeof window.getSelection !== "undefined") {
        var selection = window.getSelection();

        if (selection.rangeCount) {
            var node = selection.getRangeAt(0).commonAncestorContainer;
            containerElement = node.nodeType == 1 ? node : node.parentNode;
            text = selection.toString();
        }
    }
    else if (typeof document.selection !== "undefined" && document.selection.type !== "Control") {
        var textRange = document.selection.createRange();

        containerElement = textRange.parentElement();
        text = textRange.text;
    }

    return {
        text: text,
        target: containerElement
    };
}

For the second option you can use the select event handler.

document.addEventListener("select", onSelect, false);

function onSelect(e) {
    var { text } = getSelectionTextAndContainerElement();
    var path = getPathFromElement(e.target); 

    sendDataToServer({
        path: path,
        text: text 
    });       
}

For input text or textarea its better to use the select event handler.
If you will use the first option to get the selection you won't get the correct target node because of the fact that input text and textarea are built using Shadow DOM.
So its better to ignore the target node retruned from the getSelectionTextAndContainerElement function, and to use the target property of the select event.

I have created an example in jsfiddle for you.

Sagi
  • 8,972
  • 3
  • 33
  • 41
  • your jsfiddle works pretty well. but i did notice that if i select 12 then try to select 23 it fails to extend the selection to 123. any thoughts on that? – xeo Jan 13 '18 at 02:30
  • Please notice that the number selection is not realy a text selection but, a wrapping of the selected text with a span that has the same colors like a selection. I was not trying to create the whole solution because that was not the question, but to give an example that it is possible to get the selection and the path from an element. In order to complete the text selection you could read about it here: https://developer.mozilla.org/en-US/docs/Web/API/Selection. – Sagi Jan 13 '18 at 08:33
2

First Example :

<textarea id="quote" cols="50" rows="5">
The above properties are especially useful in getting any user selected text from a form field where the indices of the selection isn't already known. The following demo echoes what the user has selected from a TEXTAREA using these properties:
</textarea>
 
<div id="output"></div>
 
<script>
 
var quotearea = document.getElementById('quote')
var output = document.getElementById('output')
quotearea.addEventListener('mouseup', function(){
    if (this.selectionStart != this.selectionEnd){ // check the user has selected some text inside field
        var selectedtext = this.value.substring(this.selectionStart, this.selectionEnd)
        output.innerHTML = selectedtext
    }
}, false)
 
</script>

Second Example

<head>
    <script type="text/javascript">
        function GetSelectedText () {
            var selText = "";
            if (window.getSelection) {  // all browsers, except IE before version 9
                if (document.activeElement && 
                        (document.activeElement.tagName.toLowerCase () == "textarea" || 
                         document.activeElement.tagName.toLowerCase () == "input")) 
                {
                    var text = document.activeElement.value;
                    selText = text.substring (document.activeElement.selectionStart, 
                                              document.activeElement.selectionEnd);
                }
                else {
                    var selRange = window.getSelection ();
                    selText = selRange.toString ();
                }
            }
            else {
                if (document.selection.createRange) {       // Internet Explorer
                    var range = document.selection.createRange ();
                    selText = range.text;
                }
            }
            if (selText !== "") {
                alert (selText);
            }
        }
    </script>
</head>
<body onmouseup="GetSelectedText ()">
    Some text for selection.
    <br /><br />
    <textarea>Some text in a textarea element.</textarea>
    <input type="text" value="Some text in an input field." size="40"/>
    <br /><br />
    Select some content on this page!
</body>

Third Example :

<head>
    <script type="text/javascript">
        function GetSelection () {
            var selection = "";

            var textarea = document.getElementById("myArea");
            if ('selectionStart' in textarea) {
                    // check whether some text is selected in the textarea
                if (textarea.selectionStart != textarea.selectionEnd) {
                    selection = textarea.value.substring  (textarea.selectionStart, textarea.selectionEnd);
                }
            }
            else {  // Internet Explorer before version 9
                    // create a range from the current selection
                var textRange = document.selection.createRange ();
                    // check whether the selection is within the textarea
                var rangeParent = textRange.parentElement ();
                if (rangeParent === textarea) {
                    selection = textRange.text;

                }
            }

            if (selection == "") {
                alert ("No text is selected.");
            }
            else {
                alert ("The current selection is: " + selection);
            }
        }
    </script>
</head>
<body>
    <textarea id="myArea" spellcheck="false">Select some text within this field.</textarea>
    <button onclick="GetSelection ()">Get the current selection</button>
</body>
Nims Patel
  • 1,048
  • 9
  • 19
1

my idea is to add <span > at the start and end of selected text after that when you save the document whole html is saved into the database so when he retrieves the record back highlighted text will remain.

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<p>this is a paragraph creted to demonstrate highlighting selected text</p>
<script>
$(document).ready(function(){

$("p").on("mouseup",function() {
    oldtxt = chosenText();
    
        var newtxt = '<span  style="color:red;">' + oldtxt +'</span>';
$(this).html($(this).html().replace(oldtxt,newtxt));
    
});

//Grab selected text
function chosenText(){
    if(window.getSelection){
        return window.getSelection().toString();
    }
    else if(document.getSelection){
        return document.getSelection();
    }
    else if(document.selection){
        return document.selection.createRange().text;
    }
}
});
</script>

it will be comfortable in jquery to add elements

jasinth premkumar
  • 1,430
  • 1
  • 12
  • 22
  • 1
    since we don't know op's context - if there are several users doing that on the same page, that might result in span salad within your html... – errand Jan 17 '18 at 09:23
  • @errand yes but , you can save in database for different users .it show correct page for every user – jasinth premkumar Jan 17 '18 at 09:26
1

Since you use a plugin for text highlighting, get the highlighted words using jQuery:

var words = $('.highlight').map(function() { return $(this).text(); });

Then put them in an array

var saved = [ ];
for (var word in words) {
    if (-1 === saved.indexOf(word)) {
        saved.push(word);
    }
}

Finally you can save them in the database. A bad (but quick) way to do this is to save the list as comma delimited, a famous SQL antipattern:

var wordList = saved.join(',');

When you retrieve the value, you split it into words, and for each word invoke the highlight plugin.

This will not work if any of the texts contain a comma. In that case you'd better save each word individually, which saves several other troubles in the end, rather than figuring out a separating character that's "unlikely" to pop up in a user text.

LSerni
  • 55,617
  • 10
  • 65
  • 107
0

You can use Array to save a selection of the user!! after that you save the whole array to your database! and when user view the site again, function compares the letter and word from array and highlight it..

UserOnE
  • 29
  • 6
  • 2
    Yes. And *how to do that*, I suspect, is the question. – David Thomas Sep 22 '13 at 17:34
  • @DavidThomas the problem is that the OP didn't say anything about the way he manages the highlighting functionality of his application, that way only him knows how to implement it! – mohammed essam Sep 22 '13 at 18:01
  • mousedown() and mouseup() events in javascript when selection start use mousedown() and when it endup the selection mouseup() and save the selected text in array! – UserOnE Sep 22 '13 at 18:32
  • @AshishMishra There's also `selectionstart()` and `selectionend()` and you can use the answer in http://stackoverflow.com/questions/6251937/how-to-get-selecteduser-highlighted-text-in-contenteditable-element-and-replac to get the actually highlighted text ;) – mohammed essam Sep 22 '13 at 18:42
0

You can use .serialize() method which return text string in standard URL-encoded notation. it has selected individual form element such as <input>, <textarea> etc..so push serialized return string in DB using $(form).serialize(); and for highlighting the changes check Old $(form).serialize(); return value with New $(form).serialize(); return value.

DHARMENDRA SINGH
  • 607
  • 5
  • 21
0

from a testing perspective, there is no way to store detached highlights if it is possible to alter the original html without also adjusting the highlights.

my solution would be to serialize the entire colored html. then make a scrub function to remove all color highlights and return to the baseline html. this allows the html in the db to include the color highlights and editing can still occur with highlighting preserved.

something like:

function unhighlight() {
  $('.highlighted').each(function(index, el) {
    $(el).replaceWith($(el).html());
  });
}

jsfiddle: https://jsfiddle.net/tobtedsc/5/

xeo
  • 807
  • 2
  • 7
  • 25