What I need to do is grab the first <p> tag within an element, loop through all of the words, and wrap them in <span> tags. I wrote a script for this, which I thought worked, but it appears to break when some characters are in the <p> tag. I don't know which character(s) that causes it to break though.
Here is my current code:
$(document).ready(function(){
// Transform is set on html tag by modernizr
// Apply this on all .quote boxes, even if there are multiple (most likely aren't)
$('.csstransforms .quote').each(function() {
// Get data
var elem = $(this).find('p:first'),
html = elem.text(),
words = html.split(" "),
charCount = html.length
$(this).append('<p class="fixed"></p>');
// Add new words
var tmpWord = '';
for(i=0; i< words.length ; i++) {
tmpWord = $.trim(words[i]);
if(tmpWord && tmpWord != "") {
// Maybe replace with $(elem).next('.fixed') or something?
$('.csstransforms .quote .fixed').append('<span>'+ tmpWord +' </span>');
}
}
// Check word count, make size smaller if needed
if(charCount > 150) {
// Add class to .quote box
$(this).addClass('smaller');
}
// Hide original <p>
$(elem).hide();
});
});
The error i'm getting is as follows, and what you see in the text is the actual quote:
Uncaught Error: Syntax error, unrecognized expression: "In the decade or so, science has discovered a tremendous amount about the role emotions play in our lives. Researchers have found that even more than IQ, your emotional awareness and abilities to handle feelings, will determine your success and happiness in all walks of life, including family relationships". – John Gottman, Ph. D.
Any ideas as to what is causing this, and how to fix it? Been chewing on it on a while without success.
Update: Jsfiddle showing same error: http://jsfiddle.net/Uugbc/
Just for clarification. Your fiddle has
charCount = $(html).text().length;
but your variable html is not a jQuery object.
This will work better
var elem = $(this).find('p:first'),
html = elem.text(),
words = html.split(" "),
charCount = html.length; // here you had $(html).text().length;
Related
I have seen multiple posts on here finding and highlighting strings but none have worked as expected yet. Below is my script currently:
var str = 'word';
var divs= document.getElementsByClassName('strings');
for (var i = 0, len = divs.length; i < len; ++i) {
if(divs[i].innerHTML.indexOf(str) !== -1) {
// something
console.log('YES');
str.replace(/(\w+) (\w+)/, '<div class="strings">$1</div> <div class="strings">$2</div>');
}else{
console.log('NO');
}
}
HTML:
<div class="strings">word word words</div>
Ideally this would highlight every instance of the string in my div each time the js is run as a function.
The code you posted is on the right track and regex replace is convenient, but be very careful that, in addition to using the correct logic, you're not opening yourself up to XSS attacks or regex escaping problems by sanitizing your input field (although the XSS issue would be mainly problematic if the user supplies the target/source text).
Use the "gi" flags on the regex to make your search case-insensitive (I used a checkbox to toggle), and feel free to loop over multiple text areas you'd like to search when updating (I left it as one for simplicity). Add \b to the regex to enforce strict word boundaries (also toggleable in the below example). You can also use basically any element or styling on your highlighted element. <mark> seems most semantic.
Lastly, it's worth ensuring that the search term doesn't consist of an empty string, which would add a lot of garbage highlight tags between every character of the text.
const escapeHTML = html => {
const ta = document.createElement("textarea");
ta.textContent = html;
return ta.innerHTML;
};
const escapeRegex = s => s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
const highlight = (searchTerm, originalText, caseIns, boundaries) => {
const pattern = boundaries ? `(\\b${searchTerm}\\b)` : `(${searchTerm})`;
return searchTerm ? originalText.replace(
RegExp(pattern, "g" + (caseIns ? "i" : "")), "<mark>$1</mark>"
) : originalText;
};
const output = document.querySelector("#output");
const originalText = output.innerText;
let caseIns = false;
let boundaries = false;
let searchTerm = "";
document.querySelector("#ignore-case").addEventListener("change", e => {
caseIns = e.target.checked;
output.innerHTML = highlight(searchTerm, originalText, caseIns, boundaries);
});
document.querySelector("#word-boundaries").addEventListener("change", e => {
boundaries = e.target.checked;
output.innerHTML = highlight(searchTerm, originalText, caseIns, boundaries);
});
document.querySelector("input").addEventListener("keyup", e => {
searchTerm = escapeHTML(escapeRegex(e.target.value));
output.innerHTML = highlight(searchTerm, originalText, caseIns, boundaries);
});
div:first-child {
display: flex;
align-items: center;
margin-bottom: 1em;
}
span {
margin-left: 1em;
}
mark { /* add styling here */
border-radius: 2px;
}
<div>
<input placeholder="search term" />
<span>Ignore case? <input type="checkbox" id="ignore-case" /></span>
<span>Word boundaries? <input type="checkbox" id="word-boundaries" /></span>
</div>
<div id="output">Fourscore and seven years ago our fathers brought forth, on this continent, a new nation, conceived in liberty, and dedicated to the proposition that all men are created equal. Now we are engaged in a great civil war, testing whether that nation, or any nation so conceived, and so dedicated, can long endure. We are met on a great battle-field of that war. We have come to dedicate a portion of that field, as a final resting-place for those who here gave their lives, that that nation might live. It is altogether fitting and proper that we should do this. But, in a larger sense, we cannot dedicate, we cannot consecrate—we cannot hallow—this ground. The brave men, living and dead, who struggled here, have consecrated it far above our poor power to add or detract. The world will little note, nor long remember what we say here, but it can never forget what they did here. It is for us the living, rather, to be dedicated here to the unfinished work which they who fought here have thus far so nobly advanced. It is rather for us to be here dedicated to the great task remaining before us—that from these honored dead we take increased devotion to that cause for which they here gave the last full measure of devotion—that we here highly resolve that these dead shall not have died in vain—that this nation, under God, shall have a new birth of freedom, and that government of the people, by the people, for the people, shall not perish from the earth.</div>
You're using the replace() method on the needle and not on the hay stack. You want to find the str in the innerHTML attribute and then replace the innerHTML attribute with a copy that is surrounding the given str found with `' tags.
Because you're using a variable for the regex search you need to first make a regex object and inject the needle string into it. also give it the g flag so it matches every instance of the found regex:
var regex = new RegExp(`${str}`, ['g']);
Then you manipulate the innerHTML attribute of the div element:
divs[i].innerHTML = divs[i].innerHTML.replace(regex, `<span class"highlighted">${str}</span>`);
Now the script will look for the word and wrap it a span with .highlighted class.
So now all that's left to do is fix the css to handle it:
.highlighted {
background-color: yellow;
}
Sorry for the odd title, I'm sure someone has asked something similar before. My question is, I'm building a string with html tags enclosed, my question is is it better to test and add extra tags, in this case a <br />, or to add the tag anyway and have a 'remove' line at the end, which is faster?:
So at the moment we have
bFirst = true;
label = '';
if(...)
{
if (!bFirst)
label += '<br/>';
label+= 'some more text'
}
if(...)
{
if (!bFirst)
label += '<br/>';
label+= 'some more text'
}
and so on...
or
if()
{
label+= 'some more text <br />'
}
and then just remove the last <br /> using the string.substring() method.
Which is faster, better, more correct? I don't need code it is really a performance question, I could test it but I'm sure someone has done this before.
Thanks.
You can define which way is the fastest with this procedure :
Requirements :
You will need an addon like firebug or pagespeed
You'll have to execute those two pages :
Add all extra text and then trim some :
<div id ="concat"></div>
<script type="text/javascript">
var someText = ""
// adds "foo" 10k times
for (i = 0; i < 10000; i++) {
someText += "foo"
}
// crops the 3 last characters off the string 5k times
for (i = 0; i < 5000; i++) {
someText.substr(someText.length - 3, someText.length);
}
// append the final string
$('#concat').append(someText);
</script>
On my browser, it takes between 0.19 sec and 0.30 sec.
Add extra text when it is needed
<div id ="if"></div>
<script type="text/javascript">
var someText = ""
var append = true;
for (i = 0; i < 15000; i++) {
//adds "foo" the first 10k times
if(append == true){
someText += "foo";
}
if(i == 10000){
append = false;
}
}
$('#if').append(someText);
</script>
This code iterates 15k times and only adds "foo" the first 10k times.
The execution of such code can take between 0.10 and 0.12 sec
Conclusion
Adding content to your string only when it is needed seems a better option in terms of performance.
Also, in terms of code readabilty, the second option should be used.
Quick and simple analysis:
First option makes a comparison and a memory reallocation.
Second option (supposing you'd be doing label = label.substring(...)) counts characters, if you want the first N characters, String.prototype.substring() will probably count N times while reading N bytes of characters. Finally, you allocate/reallocate memory.
Now which strategy appears more efficient?
If you're still in doubt try both methods under a timer.
I count the words in a contenteditable. I split it using spaces. The problem comes when you enter a new line. It doesn’t count the word you’re currently writing on the new line until you add a space.
On top of that, in the following example if you split the example text into two lines, it will “eat up” one word when you do that:
http://jsfiddle.net/MrbUK/
I’m guessing this issue exists because between HTML elements there are no spaces:
<div>some things</div><div>are cool</div> its string would be “some thingsare cool”.
Here’s the code that I have:
function wordCount() {
var content_text = $('#post_content').text(),
char_count = content_text.length,
word_count = 0;
// if no characters, words = 0
if (char_count != 0)
word_count = content_text.replace(/[^\w ]/g, "").split(/\s+/).length;
$('.word_count').html(word_count + " words • " + char_count + " characters");
}
I tried replacing some HTML tags:
word_count = content_text.replace(/ /g, " ").replace(/<div>/g, "<p>").replace(/<\/div>/g, "</p>").replace(/<\/p><p>/g, " ").split(/\s+/).length;
without any luck. I need to discard whether it’s a <p> or <div> and some browsers add when merging lines together.
Any ideas? Thanks!
EDIT:
Thanks to Jefferson below for his clever method, I managed to solve this. For some reason I have to do -1 on the word_count to display the correct number of words:
function wordCount() {
var content_div = $('#post_content'),
content_text,
char_count = content_div.text().length,
word_count = 0;
// if no characters, words = 0
if (char_count != 0)
content_div.children().each(function(index, el) {
content_text += $(el).text()+"\n";
});
// if there is content, splits the text at spaces (else displays 0 words)
if (typeof content_text !== "undefined")
word_count = content_text.split(/\s+/).length - 1;
$('.word_count').html(word_count + " words • " + char_count + " characters");
}
You can use this:
$("#post_content").children().each(function(index, el){buffer += $(el).text()+"\n"})
This way you iterate by all elements inside your div and get only the text, put a "\n" between them.
Jefferson's answer was great, and it helped me with this exact same issue.
The problem I encountered was the contents of my contenteditable div was not entirely wrapped in HTML tags.
For example, my div contained the following HTML code:
This is my first line<div>This is my second line</div>
By using $.children(), it was ignoring the first line and only returning a word count of 5. To get round this I used $.contents() instead. Modified code is below:
$("#post_content").contents().each(function(index, el){buffer += $(el).text()+"\n"})
This returned a line count of 10.
Apologies for adding this as an answer and not as a comment to Jefferson's answer, however my reputation is too low to allow me to do that. I felt it was worth pointing the above out though.
String(s) is dynamic
It is originated from onclick event when user clicks anywhere in dom
if string(s)'s first part that is:
"login<b>user</b>account"
is enclosed in some element like this :
"<div>login<b>user</b>account</div>",
then I can get it with this:
alert($(s).find('*').andSelf().not('b,i').not(':empty').first().html());
// result is : login<b>user</b>account
But how can i get the same result in this condition when it is not enclosed in any element .i.e. when it is not enclosed in any element?
I tried this below code which works fine when first part do not include any <b></b> but it only gives "login" when it does include these tags.
var s = $.trim('login<b>user</b> account<tbody> <tr> <td class="translated">Lorem ipsum dummy text</td></tr><tr><td class="translated">This is a new paragraph</td></tr><tr><td class="translated"><b>Email</b></td></tr><tr><td><i>This is yet another text</i></td> </tr></tbody>');
if(s.substring(0, s.indexOf('<')) != ''){
alert(s.substring(0, s.indexOf('<')));
}
Note:
Suggest a generic solution that is not specific for this above string only. It should work for both the cases when there is bold tags and when there ain't any.
So it's just a b or a i, heh?
A recursive function is always the way to go. And this time, it's probably the best way to go.
var s = function getEm(elem) {
var ret = ''
// TextNode? Great!
if (elem.nodeType === 3) {
ret += elem.nodeValue;
}
else if (elem.nodeType === 1 &&
(elem.nodeName === 'B' || elem.nodeName === 'I')) {
// Element? And it's a B or an I? Get his kids!
ret += getEm(elem.firstChild);
}
// Ain't nobody got time fo' empty stuff.
if (elem.nextSibling) {
ret += getEm(elem.nextSibling);
}
return ret;
}(elem);
Jsfiddle demonstrating this: http://jsfiddle.net/Ralt/TZKsP/
PS: Parsing HTML with regex or custom tokenizer is bad and shouldn't be done.
You're trying to retrieve all of the text up to the first element that's not a <b> or <i>, but this text could be wrapped in an element itself. This is SUPER tricky. I feel like there's a better way to implement whatever it is you're trying to accomplish, but here's a solution that works.
function initialText(s){
var test = s.match(/(<.+?>)?.*?<(?!(b|\/|i))/);
var match = test[0];
var prefixed_element = test[1];
// if the string was prefixed with an element tag
// remove it (ie '<div> blah blah blah')
if(prefixed_element) match = match.slice(prefixed_element.length);
// remove the matching < and return the string
return match.slice(0,-1);
}
You're lucky I found this problem interesting and challenging because, again, this is ridiculous.
You're welcome ;-)
Try this:
if (s.substring(0, s.indexOf('<')) != '') {
alert(s.substring(0, s.indexOf('<tbody>')));
}
Below is Example string that I have.
143s: WHAT IS <span>THAT</span>? 144s: HEAR THAT? 152s: EVERYBODY, SHH. SHH. 156s: <span>STAY</span> UP THERE. 163s: [BOAT CREAKING] 165s: WHAT IS THAT? 167s: [SCREAMING] 191s: COME ON! 192s: OH, GOD! 193s: AAH! 249s: OK. WE'VE HAD SOME PROBLEMS 253s: AT THE FACILITY. 253s: WHAT WE'RE ATTEMPTING TO <span>ACHIEVE</span> 256s: HERE HAS <span>NEVER</span> BEEN DONE. 256s: WE'RE THIS CLOSE 259s: TO THE REACTIVATION 259s: OF A HUMAN BRAIN CELL. 260s: DOCTOR, THE 200 MILLION 264s: I'VE SUNK INTO THIS COMPANY 264s: IS DUE IN GREAT PART 266s: TO YOUR RESEARCH.
Consider , string with ns: and text, after is, as a single line.
e.g. 259s: OF A HUMAN BRAIN CELL.
I need regular expression , which returns me lines having ,
Previous Line of Line having span tag if any + Line having span tag + Next Line of having span tag if any
So above string should return me 3 matches.
1st : 143s: WHAT IS <span>THAT</span>? 144s: HEAR THAT?
2nd : 152s: EVERYBODY, SHH. SHH. 156s: <span>STAY</span> UP THERE. 163s: [BOAT CREAKING]
3rd : 253s: WHAT WE'RE ATTEMPTING TO <span>ACHIEVE</span> 256s: HERE HAS <span>NEVER</span> BEEN DONE
"Previous line having…" is a condition that would need lookbehind, which is not supported by JS. However, the regex would have been overly complicated, so instead just parse it and loop through the lines checking for your matches.
var text = "…";
var lines = [],
textparts = text.split(/(\d+s:)/);
for (var i=1; i<textparts.length; i+=2)
lines[(i-1)/2] = {
lineNumber: textparts[i].match(/\d+/)[0],
text: textparts[i+1],
hasSpan: /<span>/.test(textparts[i+1])
};
var matchedlines = [];
for (var i=0; i<lines.length; i++)
if (lines[i-1] && lines[i-1].hasSpan && lines[i].hasSpan && …) // or whatever
matchedlines.push(lines[i]);