How to highlight an editable word in dynamically generated text? - javascript

Intro
I am creating a content editor in which I want to add the functionality to choose a word which you would like to be highlighted while typing your content.
At this moment I achieved to search any word chosen in the #dynamicWord and then typed in #contentAreaContainer and give it a red border by adding em around the keyword and style the em trough CSS:
Part of the Code:
<div class="word">
Dynamic word to highlight: <input name="dynamic_word" id="dynamicWord" value="Enter word..">
</div>
<div id="contentAreaContainer" oninput="highlighter()">
<textarea id="contentArea"></textarea>
</div>
function highlighter()
{
var contentAreaContainer = document.getElementById('contentAreaContainer');
var dynamicWord = document.getElementById('dynamicWord').value;
wrapWord(contentAreaContainer, dynamicWord);
};
wrapWord() does:
function wrapWord(el, word)
{
var expr = new RegExp(word, "i");
var nodes = [].slice.call(el.childNodes, 0);
for (var i = 0; i < nodes.length; i++)
{
var node = nodes[i];
if (node.nodeType == 3) // textNode
{
var matches = node.nodeValue.match(expr);
if (matches)
{
var parts = node.nodeValue.split(expr);
for (var n = 0; n < parts.length; n++)
{
if (n)
{
var em = el.insertBefore(document.createElement("em"), node);
em.appendChild(document.createTextNode(matches[n - 1]));
}
if (parts[n])
{
el.insertBefore(document.createTextNode(parts[n]), node);
}
}
el.removeChild(node);
}
}
else
{
wrapWord(node, word);
}
}
}
em{border: 1px solid red;}
The problem:
Now at this moment every time on input in #contentAreaContainer the keyword chosen is highlighted a short period in the #contentAreaContainer, because highlighter() is triggered on input. But it should stay highlighted after finding it instead of only oninput.
I need oninput to search for the #dynamicWord value with wrapWord() while some one is typing;
Any time the #dynamicWord value was found it should permanently get an em
So how can I sort of 'save' the found keywords and permanently give them the element until the dynamic keyword gets edited?
Check the DEMO version
Solved:
Using setTimeout() instead of oninput I managed to make the highlight look constant. The change:
function highlighter()
{
var contentAreaContainer = document.getElementById('contentAreaContainer');
var mainKeyword = document.getElementById('main_keyword').value;
wrapWord(contentAreaContainer, mainKeyword);
repeater = setTimeout(highlighter, 0.1);
}
highlighter();
I removed oninput="highlighter()" from #contentAreaContainer.

You are trying to highlight words in a textarea. As far as I know a textarea does not support html elements inside. If you do it would simply display them as text.
Therefore you need to use an editable div. This is a normal div but if you add the attribute:
contentEditable="true"
the div acts like a textarea with the only difference it now process html elements. I also needed to change the onchange event into the onkeyup event. The editable div does not support onchange events so the highlight would not be triggered. The HTML for this div looks like:
<div contentEditable="true" id="contentArea">Test text with a word in it</div>
Here is the working code in a fiddle: http://jsfiddle.net/Q6bGJ/ When you enter a new character in the textarea your keyword gets highlighted.
However there is still a problem left. You surround the keyword with an em element. This results in surrounding it on every keystroke. Now you end up width many em's around the keyword. How to solve this, I leave up to you as a challenge.

Related

How to get column number based on text

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.
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:
Make a long content cross several pages to visualize paging (with css: column-width).
Click on previous / next to browse pages.
Input and click 'find' button, make found texts highlighted.
List all columns (pages) found with input text.
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>Found in Column ${columns[i]+1}</li>`);
/// remove dom. keep dom tree clean
while (doms.length > 0) {
var dom = doms.pop();
dom.parentNode.removeChild(dom);
}
});
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.

Selecting parts of div text via the code

Is it possible to select specific text inside a div using the code. I have a div of text, and I need to iterate through each word selecting it individually, then deslecting and onto the next word.
I'm not talking about simulating a select by changing the background css of the words requiring highlighting, but actually selecting it so the outcome is the same as if the user used the mouse to select it.
I know it's possible inside a text area input, but is it possible on a Div?
------------------------UPDATE------------------------------------------
Ok this is where I'm at with it after having another look at it today. I can select all the text in a span, but not specifically a range of words within that span. The closest I have come ( code shown below... ) is selecting the range manually, then removing the selection, then reapplying it.
<div contentEditable = 'true' id="theDiv">
A selection of words but only from here to here to be selected
</div>
<script type="text/javascript">
setInterval( function() {
var currSelection = window.getSelection();
var storedSelections = [];
if ( currSelection ) {
for (var i = 0; i < currSelection.rangeCount; i++) {
storedSelections.push (currSelection.getRangeAt (i));
}
currSelection.removeAllRanges ();
}
if ( storedSelections.length != 0 ) {
currSelection.addRange( storedSelections[0] )
}
}, 1000 );
</script>
The stored selection range object has a startOffset and endOffset property. My question is how do I set this alongside the initial selection via the code ( not via a mouse select ) ?
Please read this article, it's difficult to summarise, so better read it:
http://www.quirksmode.org/dom/range_intro.html
This answer here can be useful too:
Can you set and/or change the user’s text selection in JavaScript?
Turns out it's fairly straightforward...
<div id = "theDiv">adsf asdf asdf asdf</div>
<script type="text/javascript">
var theDiv = document.getElementById('theDiv')
theDivFirstChild = theDiv.firstChild;
var range = document.createRange();
range.setStart( theDivFirstChild, 2 );
range.setEnd( theDivFirstChild, 8);
window.getSelection().addRange(range);
</script>

Highlight all comma's in page with jquery

I have a very very simple question and i didn't find my answer.
I have a page that is using ajax and it gets update again and again in one div of it.
Now, i want to Highlight ALL the commas , of that div.
For example, they get red color.
How can i do it with this ajax page ?
I also wanted to try with this code, but i coudn't
$(document":contains(',')").css("color","red");
I just need to find all the commas in that div every second and give a style to them .
How to do it with jquery?
Don't know about jQuery, but it can be done with pure javascript. But it's not so easy actually.
tl;dr jsFiddle
This answer does not cause DOM revalidation and does not mess-up with javascript events!
First you need to loop through page content and replace every comma (or every character) with a <span> or other node so that you can give it individual CSS style. Let's start with getting textNodes:
HTMLElement.prototype.getTextNodes = function(recursive, uselist) {
var list = this.childNodes;
var nodes = uselist!=null?uselist:[];
for(var i=0,l=list.length; i<l;i++) {
if(list[i].nodeType==Node.TEXT_NODE) {
nodes.push(list[i]);
}
else if(recursive==true&&list[i].nodeType==1&&list[i].tagName.toLowerCase()!="script"&&list[i].tagName.toLowerCase()!="style") {
list[i].getTextNodes(true, nodes);
}
}
//console.log(nodes);
return nodes;
}
You'll now need to split the spans wherever the commas are:
/*Turn single text node into many spans containing single letters */
/* #param
textNode - HTMLTextNode element
highlight - the character to highlight
#return
null
*/
function replaceLetters(textNode, highlight) {
//Get the string contained in the text node
var text = textNode.data;
//Generate a container to contain text-node data
var container = document.createElement("span");
//Create another span for every single letter
var tinyNodes = [];
//Split the letters in spans
for(var i=0,l=text.length;i<l; i++) {
//skip whitespace
if(text[i].match(/^\s*$/)) {
container.appendChild(document.createTextNode(text[i]));
}
//Create a span with the letter
else {
//Create a span
var tiny = document.createElement("span");
//If the letter is our character
if(text[i]==highlight)
tiny.className = "highlighted";
tiny.innerHTML = text[i];
container.appendChild(tiny);
}
}
//replace text node with a span
textNode.parentNode.insertBefore(container, textNode);
textNode.parentNode.removeChild(textNode);
}
The function above was originaly used for animating all letters on a page (even when it was already loaded). You only need to change color of some of these.
If the functions above are defined, call this:
var nodes = document.getElementById("myDiv").getTextNodes(true);
for(var i=0, l=nodes.length; i<l; i++) {
replaceLetters(nodes[i], ",");
}
You need to wrap the commas with an HTML tag (such as <span>).
$(window).load(function() {
$('.target').each(function() {
var string = $(this).html();
$(this).html(string.replace(/,/g , '<span class="comma">,</span>'));
});
});
Here is an example:
http://jsfiddle.net/5mh6ja1L/
I don't know how to do it in jQuery but in pure JavaScript it would be something like this:
var el = document.getElementById("content");
el.innerHTML = el.innerHTML.replace(/,/g, "<b class='highlight'>,</b>");
demo: http://jsfiddle.net/f9xs0c79/
No need to do loop. You can just select the container where you want to replace and replace.
$("p").html(
$("p").html().replace(/,/g,"<span class='comma'>,</span>")
);
http://jsfiddle.net/1570ya75/2/

replace specific tag name javascript

I want to know if we can change tag name in a tag rather than its content. i have this content
< wns id="93" onclick="wish(id)">...< /wns>
in wish function i want to change it to
< lmn id="93" onclick="wish(id)">...< /lmn>
i tried this way
document.getElementById("99").innerHTML =document.getElementById("99").replace(/wns/g,"lmn")
but it doesnot work.
plz note that i just want to alter that specific tag with specific id rather than every wns tag..
Thank you.
You can't change the tag name of an existing DOM element; instead, you have to create a replacement and then insert it where the element was.
The basics of this are to move the child nodes into the replacement and similarly to copy the attributes. So for instance:
var wns = document.getElementById("93");
var lmn = document.createElement("lmn");
var index;
// Copy the children
while (wns.firstChild) {
lmn.appendChild(wns.firstChild); // *Moves* the child
}
// Copy the attributes
for (index = wns.attributes.length - 1; index >= 0; --index) {
lmn.attributes.setNamedItem(wns.attributes[index].cloneNode());
}
// Replace it
wns.parentNode.replaceChild(lmn, wns);
Live Example: (I used div and p rather than wns and lmn, and styled them via a stylesheet with borders so you can see the change)
document.getElementById("theSpan").addEventListener("click", function() {
alert("Span clicked");
}, false);
document.getElementById("theButton").addEventListener("click", function() {
var wns = document.getElementById("target");
var lmn = document.createElement("p");
var index;
// Copy the children
while (wns.firstChild) {
lmn.appendChild(wns.firstChild); // *Moves* the child
}
// Copy the attributes
for (index = wns.attributes.length - 1; index >= 0; --index) {
lmn.attributes.setNamedItem(wns.attributes[index].cloneNode());
}
// Insert it
wns.parentNode.replaceChild(lmn, wns);
}, false);
div {
border: 1px solid green;
}
p {
border: 1px solid blue;
}
<div id="target" foo="bar" onclick="alert('hi there')">
Content before
<span id="theSpan">span in the middle</span>
Content after
</div>
<input type="button" id="theButton" value="Click Me">
See this gist for a reusable function.
Side note: I would avoid using id values that are all digits. Although they're valid in HTML (as of HTML5), they're invalid in CSS and thus you can't style those elements, or use libraries like jQuery that use CSS selectors to interact with them.
var element = document.getElementById("93");
element.outerHTML = element.outerHTML.replace(/wns/g,"lmn");
FIDDLE
There are several problems with your code:
HTML element IDs must start with an alphabetic character.
document.getElementById("99").replace(/wns/g,"lmn") is effectively running a replace command on an element. Replace is a string method so this causes an error.
You're trying to assign this result to document.getElementById("99").innerHTML, which is the HTML inside the element (the tags, attributes and all are part of the outerHTML).
You can't change an element's tagname dynamically, since it fundamentally changes it's nature. Imagine changing a textarea to a select… There are so many attributes that are exclusive to one, illegal in the other: the system cannot work!
What you can do though, is create a new element, and give it all the properties of the old element, then replace it:
<wns id="e93" onclick="wish(id)">
...
</wns>
Using the following script:
// Grab the original element
var original = document.getElementById('e93');
// Create a replacement tag of the desired type
var replacement = document.createElement('lmn');
// Grab all of the original's attributes, and pass them to the replacement
for(var i = 0, l = original.attributes.length; i < l; ++i){
var nodeName = original.attributes.item(i).nodeName;
var nodeValue = original.attributes.item(i).nodeValue;
replacement.setAttribute(nodeName, nodeValue);
}
// Persist contents
replacement.innerHTML = original.innerHTML;
// Switch!
original.parentNode.replaceChild(replacement, original);
Demo here: http://jsfiddle.net/barney/kDjuf/
You can replace the whole tag using jQuery
var element = $('#99');
element.replaceWith($(`<lmn id="${element.attr('id')}">${element.html()}</lmn>`));
[...document.querySelectorAll('.example')].forEach(div => {
div.outerHTML =
div.outerHTML
.replace(/<div/g, '<span')
.replace(/<\/div>/g, '</span>')
})
<div class="example">Hello,</div>
<div class="example">world!</div>
You can achieve this by using JavaScript or jQuery.
We can delete the DOM Element(tag in this case) and recreate using .html or .append menthods in jQuery.
$("#div-name").html("<mytag>Content here</mytag>");
OR
$("<mytag>Content here</mytag>").appendTo("#div-name");

Tag-like autocompletion and caret/cursor movement in contenteditable elements

I'm working on a jQuery plugin that will allow you to do #username style tags, like Facebook does in their status update input box.
My problem is, that even after hours of researching and experimenting, it seems REALLY hard to simply move the caret. I've managed to inject the <a> tag with someone's name, but placing the caret after it seems like rocket science, specially if it's supposed work in all browsers.
And I haven't even looked into replacing the typed #username text with the tag yet, rather than just injecting it as I'm doing right now... lol
There's a ton of questions about working with contenteditable here on Stack Overflow, and I think I've read all of them, but they don't really cover properly what I need. So any more information anyone can provide would be great :)
You could use my Rangy library, which attempts with some success to normalize browser range and selection implementations. If you've managed to insert the <a> as you say and you've got it in a variable called aElement, you can do the following:
var range = rangy.createRange();
range.setStartAfter(aElement);
range.collapse(true);
var sel = rangy.getSelection();
sel.removeAllRanges();
sel.addRange(range);
I got interested in this, so I've written the starting point for a full solution. The following uses my Rangy library with its selection save/restore module to save and restore the selection and normalize cross browser issues. It surrounds all matching text (#whatever in this case) with a link element and positions the selection where it had been previously. This is triggered after there has been no keyboard activity for one second. It should be quite reusable.
function createLink(matchedTextNode) {
var el = document.createElement("a");
el.style.backgroundColor = "yellow";
el.style.padding = "2px";
el.contentEditable = false;
var matchedName = matchedTextNode.data.slice(1); // Remove the leading #
el.href = "http://www.example.com/?name=" + matchedName;
matchedTextNode.data = matchedName;
el.appendChild(matchedTextNode);
return el;
}
function shouldLinkifyContents(el) {
return el.tagName != "A";
}
function surroundInElement(el, regex, surrounderCreateFunc, shouldSurroundFunc) {
var child = el.lastChild;
while (child) {
if (child.nodeType == 1 && shouldSurroundFunc(el)) {
surroundInElement(child, regex, surrounderCreateFunc, shouldSurroundFunc);
} else if (child.nodeType == 3) {
surroundMatchingText(child, regex, surrounderCreateFunc);
}
child = child.previousSibling;
}
}
function surroundMatchingText(textNode, regex, surrounderCreateFunc) {
var parent = textNode.parentNode;
var result, surroundingNode, matchedTextNode, matchLength, matchedText;
while ( textNode && (result = regex.exec(textNode.data)) ) {
matchedTextNode = textNode.splitText(result.index);
matchedText = result[0];
matchLength = matchedText.length;
textNode = (matchedTextNode.length > matchLength) ?
matchedTextNode.splitText(matchLength) : null;
surroundingNode = surrounderCreateFunc(matchedTextNode.cloneNode(true));
parent.insertBefore(surroundingNode, matchedTextNode);
parent.removeChild(matchedTextNode);
}
}
function updateLinks() {
var el = document.getElementById("editable");
var savedSelection = rangy.saveSelection();
surroundInElement(el, /#\w+/, createLink, shouldLinkifyContents);
rangy.restoreSelection(savedSelection);
}
var keyTimer = null, keyDelay = 1000;
function keyUpLinkifyHandler() {
if (keyTimer) {
window.clearTimeout(keyTimer);
}
keyTimer = window.setTimeout(function() {
updateLinks();
keyTimer = null;
}, keyDelay);
}
HTML:
<p contenteditable="true" id="editable" onkeyup="keyUpLinkifyHandler()">
Some editable content for #someone or other
</p>
As you say you can already insert an tag at the caret, I'm going to start from there. The first thing to do is to give your tag an id when you insert it. You should then have something like this:
<div contenteditable='true' id='status'>I went shopping with <a href='#' id='atagid'>Jane</a></div>
Here is a function that should place the cursor just after the tag.
function setCursorAfterA()
{
var atag = document.getElementById("atagid");
var parentdiv = document.getElementById("status");
var range,selection;
if(window.getSelection) //FF,Chrome,Opera,Safari,IE9+
{
parentdiv.appendChild(document.createTextNode(""));//FF wont allow cursor to be placed directly between <a> tag and the end of the div, so a space is added at the end (this can be trimmed later)
range = document.createRange();//create range object (like an invisible selection)
range.setEndAfter(atag);//set end of range selection to just after the <a> tag
range.setStartAfter(atag);//set start of range selection to just after the <a> tag
selection = window.getSelection();//get selection object (list of current selections/ranges)
selection.removeAllRanges();//remove any current selections (FF can have more than one)
parentdiv.focus();//Focuses contenteditable div (necessary for opera)
selection.addRange(range);//add our range object to the selection list (make our range visible)
}
else if(document.selection)//IE 8 and lower
{
range = document.body.createRange();//create a "Text Range" object (like an invisible selection)
range.moveToElementText(atag);//select the contents of the a tag (i.e. "Jane")
range.collapse(false);//collapse selection to end of range (between "e" and "</a>").
while(range.parentElement() == atag)//while ranges cursor is still inside <a> tag
{
range.move("character",1);//move cursor 1 character to the right
}
range.move("character",-1);//move cursor 1 character to the left
range.select()//move the actual cursor to the position of the ranges cursor
}
/*OPTIONAL:
atag.id = ""; //remove id from a tag
*/
}
EDIT:
Tested and fixed script. It definitely works in IE6, chrome 8, firefox 4, and opera 11. Don't have other browsers on hand to test, but it doesn't use any functions that have changed recently so it should work in anything that supports contenteditable.
This button is handy for testing:
<input type='button' onclick='setCursorAfterA()' value='Place Cursor After <a/> tag' >
Nico

Categories

Resources