DOM manipulation while preserving previous handlers - javascript

The problem:
Let's say I have a div with a text like this
<div id=”bla”>One Two Three Four Five</div>
I want to be able programmatically take any substring and wrap it into a <div> with some handler attached to it (say onlick). I want to be able to do it multiple times to different part of the text so in the end my div can end up looking like this:
<div id=”bla”>One <div id=”bla2”>Two</div> Three <div id=”bla4”>Four</div> Five</div>
The problem is how to do it?
Some thoughts:
Potentially, if I want to wrap the string Two into div, then if I just take whole div content using html(), do substring before and after Two, then do .empty() on the div and .append(beforeSubstring, <Two wrapper with some handler>, afterString) – it looks good, but it will put the beforeSubstring and afterSubstring into “” and remove all previous handlers. But I want to keep previous handlers and I don’t need “” since it messes things up for me.
Any thoughts? :)

You first need to cache your element's content like var cont = $("#bla").text();
than always use that cont before inserting your wrappers (using RegExp I suppose...) and placing the result back into that element like $("#bla").html(regexModifiedCont);
Example:
var $contentEl = $("#bla");
var content = $contentEl.text(); // Cache the original content!
$("#query").on("input", function(){
var reg = new RegExp("("+ $.trim(this.value) +")", "ig");
$contentEl.html( content.replace(reg, "<span class='clickable'>$1</span>") );
});
$("body").on("click", ".clickable", function(){
alert( $(this).text() );
});
.clickable{
background:gold;
cursor:pointer;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Write a text to take <input id=query type=text>
<div id=bla>One Two Three Four Five</div>

Perhaps you're looking for something like this? =)
function wrapTag(string, tag) {
return '<'+tag+'>' + string + '</'+tag+'>';
}
// http://stackoverflow.com/questions/5379120/get-the-highlighted-selected-text
function getSelectionText() {
var text = "";
if (window.getSelection) {
text = window.getSelection().toString();
} else if (document.selection && document.selection.type != "Control") {
text = document.selection.createRange().text;
}
return text;
}
function highlightSelection(tag) {
var highlighted = getSelectionText();
var newContent = $('#paragraph').html()
.replace(/\s+/g,' ').trim()
.replace(highlighted, wrapTag(highlighted, tag));
$('#paragraph').html(newContent);
}
Working example:
http://plnkr.co/edit/VCpGScPR9TEjuY3mtP2j?p=preview

Related

Determining a character of a sentence when clicked on

On a random break I found myself wondering if it would be possible to use jQuery to determine a single character within a sentence when it is clicked on.
For example:
This
When the user clicks on first h, jQuery would return this to me.
The only way I could think of doing this would be to wrap each character within the sentence in a span with a class of its letter such as the following example:
<span class="clickable T">T</span>
<span class="clickable h">h</span>
<span class="clickable i">i</span>
<span class="clickable s">s</span>
Followed by a $('.clickable').click(function()) that would return its second class.
My question is: is this the most efficient way to do this?
Obviously wrapping every single letter of the document in span tags is not efficient.
I was able to spin something up that works in Chrome at least. Basically, when you click on a letter, it then triggers a double clicks which selects the word. We get the selection which actually gives us the text of the entire target element. From that, we get the letter that was clicked. We remove the selection and do what we want with the letter.
Fiddle here
$(function(){
$(document).click(function(e){
var target = e.target;
$(target).dblclick();
}).dblclick(function(){
var selection,
node,
text,
start,
end,
letter;
if (window.getSelection) {
selection = document.getSelection();
node = selection.anchorNode;
if (node.nodeType === 3) {
text = node.data;
start = selection.baseOffset;
end = selection.extentOffet;
if (!isNaN(start)) {
letter = text.substr(start, 1);
}
}
window.getSelection().removeAllRanges()
} else if(document.selection) {
//continue work here
}
if (letter) {
alert(letter);
}
});
});
You could return the innerHTML as well with:
$('.clickable').on('click', function(){
alert($(this).html());
});
As for a more efficient way to do it...maybe try this:
in Javascript/jQuery, how to check a specific part of a string and determine if it is a whitespace or letter?
You can do it with this script
$('.clickable').on('click', function(){
var html = $(this).text(); // if you want the text inside the span
var index = $(this).index(); // if you want the position among siblings
var classes = $(this).attr('class').split(" ");
var secondClass = getSecondClass(classes);
});
function getSecondClass(classArray){
if(classArray.length<2){
return null;
}else{
return classArray[1];
}
}
I've also included the html and index variables if you want to do something else with the clicked element.
Basically you split the classes of the element by spaces and then check if the array has less than two elements, in that case it returns null, otherwise it returns the second element.
jsFiddle
Well wrapping all text dyanamically with span tag , it is possible what you were looking for
JS
$(function(){
var lengthText = $('#singlecharacter').text().length;
var textValue = $('#singlecharacter').text();
var textArray = textValue.split('');
var newText = new Array();
for (var i = lengthText - 1; i >= 0; i--) {
newText[i] = "<span class='sp'>"+textArray[i]+"</span>";
};
$('#singlecharacter').html(newText);
$('.sp').click(function()
{
alert($(this).text());
});
});
HTML
<div id='singlecharacter'>THIS</div>
DEMO JSFIDDLE

Insert character at specific point but preserving tags?

Update #2
Okay, more testing ensues. It looks like the code works fine when I use a faux spacer, but the regex eventually fails. Specifically, the following scenarios work:
Select words above or below the a tag
You select only one line either directly above or below the a tag
You select more than one line above/below the a tag
You select more than one line specifically below any a tag
The following scenarios do not work:
You select the line/more lines above the a tag, and then the line/more lines below the a tag
What happens when it "doesn't work" is that it removes the a tag spacer from the DOM. This is probably a problem with the regex...
Basically, it fails when you select text around the a tag.
Update:
I don't need to wrap each line in a p tag, I can instead use an inline element, such as an a, span, or label tag, with display:inline-block and a height + width to act as a new line element (<br />). This should make it easier to modify the code, as the only part that should have to change is where I get the text in between the bounds. I should only have to change that part, selectedText.textContent, to retrieve the HTML that is also within the bounds instead of just the text.
I am creating a Phonegap that requires the user to select text. I need fine control over the text selection, however, and can no longer plop the entire text content in a pre styled p tag. Instead, I need represent a linebreak with something like <a class="space"></a>, so that the correct words can be highlighted precisely. When my text looks like this:
<p class="text">This is line one
Line two
Line three
</p>
And has .text{ white-space:pre-wrap }, the following code allows me to select words, then wrap the text with span elements to show that the text is highlighted:
$("p").on("copy", highlight);
function highlight() {
var text = window.getSelection().toString();
var selection = window.getSelection().getRangeAt(0);
var selectedText = selection.extractContents();
var span = $("<span class='highlight'>" + selectedText.textContent + "</span>");
selection.insertNode(span[0]);
if (selectedText.childNodes[1] != undefined) {
$(selectedText.childNodes[1]).remove();
}
var txt = $('.text').html();
$('.text').html(txt.replace(/<\/span>(?:\s)*<span class="highlight">/g, ''));
$(".output ul").html("");
$(".text .highlight").each(function () {
$(".output ul").append("<li>" + $(this).text() + "</li>");
});
clearSelection();
}
function clearSelection() {
if (document.selection) {
document.selection.empty();
} else if (window.getSelection) {
window.getSelection().removeAllRanges();
}
}
This code works beautifully, but not when each line is separated by a spacer tag. The new text looks like this:
<p class="text">
Line one
<a class="space"></a>
Line two
<a class="space"></a>
Line three
</p>
When I modify the above code to work with have new lines represented by <a class="space"></a>, the code fails. It only retrieves the text of the selection, and not the HTML (selectedText.textContent). I'm not sure if the regex will also fail with an a element acting as a new line either. The a element could be a span or label, or any normally inline positioned element, to trick iOS into allowing me to select letters instead of block elements.
Is there anyway to change the code to preserve the new line elements?
jsFiddle: http://jsfiddle.net/charlescarver/39TZ9/3/
The desired output should look like this:
If the text "Line one" was highlighted:
<p class="text">
<span class="highlight">Line one</span>
<a class="space"></a>
Line two
<a class="space"></a>
Line three
</p>
If the text "Line one Line two" was highlighted:
<p class="text">
<span class="highlight">Line one
<a class="space"></a>
Line two</span>
<a class="space"></a>
Line three
</p>
Of course different parts and individual letters can and will be highlighted as well instead of full lines.
Here is a solution that supports all the features from your requirements:
HTML:
<p class="text">
First Line
<a class="space"></a>
<a class="space"></a>
Second Line
<span class="space"></span>
Third Line
<label class="space"></label>
Forth Line
</p>
<ul class="output"></ul>
CSS:
.space {
display: inline-block;
width: 100%;
}
.highlighting {
background-color: green;
}
JavaScript:
var text,
output,
unwrapContents,
mergeElements,
clearSelection,
clearHighlighting,
mergeHighlighting,
handleCopy;
unwrapContents = function unwrapContents(element) {
while(element.firstChild !== null) {
element.parentNode.insertBefore(element.firstChild, element);
}
element.parentNode.removeChild(element);
};
mergeElements = function mergeElements(startElement, endElement) {
var currentElement;
endElement = endElement.nextSibling;
while((currentElement = startElement.nextSibling) !== endElement) {
startElement.appendChild(currentElement);
}
};
clearSelection = function clearSelection() {
if (document.selection) {
document.selection.empty();
} else if (window.getSelection) {
window.getSelection().removeAllRanges();
}
};
clearHighlighting = function clearHighlighting(target, exception) {
$('.highlighting', target).each(function(index, highlighting) {
if(highlighting !== exception) {
unwrapContents(highlighting);
}
});
target.normalize();
};
mergeHighlighting = function mergeHighlighting() {
var i, j;
// Remove internal highlights
$('.highlighting', text).filter(function() {
return this.parentNode.className === 'highlighting';
}).each(function(index, highlighting) {
unwrapContents(highlighting);
});
text.normalize();
// Merge adjacent highlights
first:
for(i=0; i<text.childNodes.length-1; i++) {
if(text.childNodes[i].className === 'highlighting') {
for(j=i+1; j<text.childNodes.length; j++) {
if(text.childNodes[j].className === 'highlighting') {
mergeElements(text.childNodes[i], text.childNodes[j--]);
unwrapContents(text.childNodes[i].lastChild);
} else {
switch(text.childNodes[j].nodeType) {
case 1:
if(text.childNodes[j].className !== 'space') {
continue first;
}
break;
case 3:
if(text.childNodes[j].textContent.trim() !== '') {
continue first;
}
break;
}
}
}
}
}
};
handleCopy = function handleCopy() {
var range,
highlighting,
item;
// Highlighting
range = window.getSelection().getRangeAt(0);
highlighting = document.createElement('span');
highlighting.className = 'highlighting';
highlighting.appendChild(range.cloneContents());
range.deleteContents();
range.insertNode(highlighting);
// Output
item = document.createElement('li');
item.innerHTML = highlighting.innerHTML;
clearHighlighting(item);
output.appendChild(item);
// Cleanup
mergeHighlighting();
clearSelection();
};
$(function(){
text = $('.text')[0];
output = $('.output')[0];
$(text).on('copy', handleCopy);
});
Here is a working example http://jsbin.com/efohit/3/edit
Well, I came up with a solution, rather straightforward as well.
If .text looks like this:
<p class="text">Line one
<a class="space"></a>Line two
<a class="space"></a>Line three</p>
With the exact markup and line breaks as above, then I can find each \n and replace it with a spacer element.
if (textStr.indexOf("\n") >= 0) {
textStr = textStr.replace(/\n/g, "\n<a class='space'></a>");
}
This isn't versatile at all, and will fail if there are more than one line breaks, if the tags are different, etc. So, I encourage anyone who has a better method to answer the question! It can't be that hard, I figured it out.

Read more/less without stripping Html tags in JavaScript

I want to implement readmore/less feature. i.e I will be having html content and I am going to show first few characters from that content and there will be a read more link in front of it. I am currently using this code :
var txtToHide= input.substring(length);
var textToShow= input.substring(0, length);
var html = textToShow+ '<span class="readmore"> … </span>'
+ ('<span class="readmore">' + txtToHide+ '</span>');
html = html + '<a id="read-more" title="More" href="#">More</a>';
Above input is the input string and length is the length of string to be displayed initially.
There is an issue with this code, suppose if I want to strip 20 characters from this string:
"Hello <a href='#'>test</a> output", the html tags are coming between and it will mess up the page if strip it partially. What I want here is that if html tags are falling between the range it should cover the full tag i.e I need the output here to be "Hello <a href='#'>test</a>" . How can I do this
Why not just hide the hidden part of the content instead of adding it later? I usually just use a display: none for hidden content and have it set to display: block when the read more is clicked..
Edit:
I'm sorry I didn't read the question good enough.
This should work though:
<div id="test">
This links to google
<strong>and</strong> some random text to make it a little bit longer!
</div>
<script type="text/javascript">
$(document).ready(function() {
var max_length = 21;
var text_to_display = "";
var index = 0;
var full_contents = $("#test").contents();
// loop through contents, stop after maxlength is reached
$("#test").contents().each(function(i) {
if ($(this).text().length + text_to_display.length < max_length) {
text_to_display += $(this).text();
index++;
} else {
return false;
}
});
// second loop removes unwanted content
$("#test").contents().each(function(i) {
if (i > index) {
$(this).remove();
}
return true;
});
// add link to show the full text
$('read more...').click(
function(){
$("#test").html($(full_contents));
$(this).hide();
}).insertAfter($("#test"));
});
</script>
This can be accomplished quite easilly using jQuery
<div id="test">This is a link to google</div>
<script type="text/javascript">
$(document).ready(function() {
alert($("#test").text());
});
</script>
Good luck!
You stated that the html tags would become an issue, so why not remove in the string conversion and replace with plain text, then on the Show More click, Toggle plain + Html
$(document).ready(function(){
var Contents = $('#post p'); //Object
var Plain = Contents.text(); //truncate this
//Hide the texts to Contents
Contents.hide();
var PlainContainer = $("<div>").addClass("Plain_Container").val(Plain)
//Add PlainContainer div after
Contents.append(PlainContainer);
var $('.show_hide').click(function(){
$(Plain_Container).remove(); //Delete it
Contents.Show(); //Show the orginal
$(this).remove(); //Remove the link
return false; //e.PreventDefault() :)
});
});
This way using the text() function will convert html tags to there values and remove the tag itself, all you have to do is toggle them :)
Note: The code above is not guaranteed to work and is provided as example only.

How to select a part of string?

How to select a part of string?
My code (or example):
<div>some text</div>
$(function(){
$('div').each(function(){
$(this).text($(this).html().replace(/text/, '<span style="color: none">$1<\/span>'));
});
});
I tried this method, but in this case is selected all context too:
$(function(){
$('div:contains("text")').css('color','red');
});
I try to get like this:
<div><span style="color: red">text</span></div>
$('div').each(function () {
$(this).html(function (i, v) {
return v.replace(/foo/g, '<span style="color: red">$&<\/span>');
});
});
What are you actually trying to do? What you're doing at the moment is taking the HTML of each matching DIV, wrapping a span around the word "text" if it appears (literally the word "text") and then setting that as the text of the element (and so you'll see the HTML markup on the page).
If you really want to do something with the actual word "text", you probably meant to use html rather than text in your first function call:
$('div').each(function(){
$(this).html($(this).html().replace(/text/, '<span style="color: none">$1<\/span>'));
// ^-- here
}
But if you're trying to wrap a span around the text of the div, you can use wrap to do that:
$('div').wrap('<span style="color: none"/>');
Like this: http://jsbin.com/ucopo3 (in that example, I've used "color: blue" rather than "color: none", but you get the idea).
$(function(){
$('div:contains("text")').each(function() {
$(this).html($(this).html().replace(/(text)/g, '<span style="color:red;">\$1</span>'));
});
});
I've updated your fiddle: http://jsfiddle.net/nMzTw/15/
The general practice of interacting with the DOM as strings of HTML using innerHTML has many serious drawbacks:
Event handlers are removed or replaced
Opens the possibility of script inject attacks
Doesn't work in XHTML
It also encourages lazy thinking. In this particular instance, you're matching against the string "text" within the HTML with the assumption that any occurrence of the string must be within a text node. This is patently not a valid assumption: the string could appear in a title or alt attribute, for example.
Use DOM methods instead. This will get round all the problems. The following will use only DOM methods to surround every match for regex in every text node that is a descendant of a <div> element:
$(function() {
var regex = /text/;
function getTextNodes(node) {
if (node.nodeType == 3) {
return [node];
} else {
var textNodes = [];
for (var n = node.firstChild; n; n = n.nextSibling) {
textNodes = textNodes.concat(getTextNodes(n));
}
return textNodes;
}
}
$('div').each(function() {
$.each(getTextNodes(this), function() {
var textNode = this, parent = this.parentNode;
var result, span, matchedTextNode, matchLength;
while ( textNode && (result = regex.exec(textNode.data)) ) {
matchedTextNode = textNode.splitText(result.index);
matchLength = result[0].length;
textNode = (matchedTextNode.length > matchLength) ?
matchedTextNode.splitText(matchLength) : null;
span = document.createElement("span");
span.style.color = "red";
span.appendChild(matchedTextNode);
parent.insertBefore(span, textNode);
}
});
});
});

Jquery Hide Class when no class is present

I have some text below called (16 Courses). I need to hide only this text, but I can't seem to hide it no matter what I try using jquery. Is there any help someone could provide so I can hide on this text?
<div id="programAttributes">
<div class="left" id="credits">
<h3>Credits</h3>
<h3 class="cost">48</h3>
(16 Courses)
</div>
<div class="gutter12 left"> </div>
<div class="left" id="costPer">
<h3>Cost Per Credit</h3>
<h3 class="cost">$300</h3>
</div>
</div>
I thought if I could write something like this that would do the trick, but I am so far unsuccessful.
$("#credits:not([class!=h3])").hide();
Usage
// hides in the whole document
hideText("(16 Courses)");
// only hide inside a specific element
hideText("(16 Courses)", $('#programAttributes'));
// make it visible again
showText("(16 Courses)");
[See it in action]
CSS
.hiddenText { display:none; }
Javascript
// escape by Colin Snover
RegExp.escape = function(text) {
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
}
function hideText(term, base) {
base = base || document.body;
var re = new RegExp("(" + RegExp.escape(term) + ")", "gi");
var replacement = "<span class='hiddenText'>" + term + "</span>";
$("*", base).contents().each( function(i, el) {
if (el.nodeType === 3) {
var data = el.data || el.textContent || el.innerText;
if (data = data.replace(re, replacement)) {
var wrapper = $("<span>").html(data);
$(el).before(wrapper.contents()).remove();
}
}
});
}
function showText(term, base) {
var text = document.createTextNode(term);
$('span.hiddenText', base).each(function () {
this.parentNode.replaceChild(text.cloneNode(false), this);
});
}
You can check for and remove textnodes like this:
​$("#credits").contents().filter(function() {
if(this.nodeType == 3)
this.parentNode.removeChild(this);
});​​​​​​
You can test it here, this gets all the nodes (including text nodes) with .contents(), then we loop through, if it's a text node (.nodeType == 3) then we remove it.
Could you wrap it in a separate span, and then do:
$('#credits span').hide();
?
Try wrapping the text in a span as follows:
<div class="left" id="credits">
<h3>Credits</h3>
<h3 class="cost">48</h3>
<span id="toHide">(16 Courses)</span>
</div>
then you can use jquery:
$("#credits > span)").hide();
the hide() function has to be applied to a DOM element.
I would use a label tag around the text so I can handle it with jquery.
It's textnode. Loop thru all parents nodes and if it's type is textnode, hide it. See also this:
How do I select text nodes with jQuery?

Categories

Resources