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.
8 Answers
Answers 1
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.
Answers 2
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.
- Simple text in the page.
- 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.
Answers 3
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
Answers 4
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.
Answers 5
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>
Answers 6
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..
Answers 7
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.
Answers 8
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/
0 comments:
Post a Comment