I have an array like this
var words = [
{
word: 'Something',
link: 'http://www.something.com'
},
{
word: 'Something Else',
link: 'http://www.something.com/else'
}
];
I want it to search the page for word and replace it with link. Is there an efficient way of doing this? It seems it may be CPU hungry.
Sorry should have explained more...
It would search each element with the class .message for instance. Then find all of the words within that class and replace it with link.
There would also be a few hundred within this array
A good strategy is:
1) Build an object whose keys are the phrases to replace and whose values are the links to replace them with.
2) While doing that, construct a regular expression that can match any of the keys, then
3) Use that regex to globally replace.
Rough example:
var replacementDict = {
'foo': 'http://www.foo.com/',
'bar': 'http://www.bar.net/'
};
var theRegex = /\b(foo|bar)\b/g;
theText.replace(theRegex, function(s, theWord) {
return "<a href='" + replacementDict[theWord] + "'>" + theWord + "</a>";
});
Given some content like:
<div class="message">Somethsg1</div>
<div class="message">Something</div>
<div class="message">Ssething</div>
<div class="message">Something Else</div>
<div class="message">Something da</div>
<div class="message">Somethin2g</div>
You can use something like:
//your array
var words = [
{
word: 'Something',
link: 'http://www.something.com'
},
{
word: 'Something Else',
link: 'http://www.something.com/else'
}
];
//iterate the array
$.each(words,
function() {
//find an element with class "message" that contains "word" (from array)
$('.message:contains("' + this.word + '")')
//substitute html with a nice anchor tag
.html('' + this.link + '');
}
);
This solution has one immediate problem though (showed in the example too). If you search for example for Something and you find Something beautiful, the "contains" will be match.
If you want a strict selection, you have to do:
//for each array element
$.each(words,
function() {
//store it ("this" is gonna become the dom element in the next function)
var search = this;
$('.message').each(
function() {
//if it's exactly the same
if ($(this).text() === search.word) {
//do your magic tricks
$(this).html('' + search.link + '');
}
}
);
}
);
It's your choice whether to iterate all array elements first then all the doms, or the other way around. It's also depends on which kind of "words" you are gonna search (See the two example for the "why").
BIG WARNING: if the array contains user-defined content, you have to sanitize it before injiecting it to the elements' html!
It would be possible to do it with something like:
$('*:contains("string to find")');
the problem with this approach is that "*" will return all elements that contain the string, including HTML, BODY, etc... and after that you still need to find the string inside the text node of each element, so it may be easier to just go and check every text node...
I'd suggest you take a look at the highlight plugin that already does something very similar to what you want (instead of linking, it highlights any text on a page), but from the source code it seems pretty easy to change it.
If you want to wrap in 'a' tag un-comment code and comment call above.
Try this:
var words = [
{
word: 'Something',
link: 'http://www.something.com'
},
{
word: 'Something Else',
link: 'http://www.something.com/else'
}];
var changeWordsWithLink = function (words) {
if(document.getElementById && document.getElementsByTagName) {
var messages = document.getElementById('message');
if(messages) {
for(i = 0; i < messages.length; i++){
for (j = 0; j < words.length; j++) {
if(words[j].word == messages[i].innerHTML) {
messages[i].innerHTML = words[j].link;
//messages[i].innerHTML = wrapInATag(words[j].link, words[j].word);
}
}
}
}
}
}
var wrapInATag = function(link, word) {
return '' + word + '';
}
Related
I have some object with label and value. Those elements holds some data.
For now i want to loop on html elements and check, what element match this elements. Object is called ui.
Right now i want to select all elements that contains text from any of object elements.
Those elements have class .img.
Below simple javascript:
$("#project").autocomplete({
source: Items,
appendTo: ".autocomplete",
minLength: 2,
response: function (event, ui) {
if (ui.content.length == 0) {
return false;
}
var content = ui.content[0].label;
$.each($('.img'), function () {
if ($(this).is(':contains(' + content + ')')) {
return;
} else {
$(this).fadeOut(100);
}
});
}
});
This code acts as autocomplete, and i want to hide all emelents that don't match data from ui object.
This code works for me almost fine, but there is one thing that break things:
var content = ui.content[0].label;
This selects only first item from object, and i'm looking for a way, how to check all items from obejct to check if text contains data, not only from first obejct element [0].
Thanks for advice.
I create a jsfiddle for you, If I understand the problem
var matchLabelString=_.reduce(ui,function(accumulateString,current){
if(accumulateString == ""){
return current.label;
}
return accumulateString + "|"+current.label;
},"")
var regex = new RegExp("^"+matchLabelString);
$.each($('.img'), function () {
if (regex.test($(this).text())) {
return;
} else {
$(this).fadeOut(100);
}
});
https://jsfiddle.net/3o1oy5y2/
EDIT: I see your code, and I think that my code work anyway. Try it, as you can see I have used underscore library to create a string seperatad by | from array
You can add a loop in your $each callback :
$.each($('.img'), function() {
for (var i = 0; i < ui.content.length; i++) {
var content = ui.content[i].label;
...
}
});
EDIT: Change let to var
This is my first Angular Directive.
I am trying to do a simple highlight on a html content based on the search terms used to find that content.
The problem is, that is working for the first term, but not for more. I want to all words get highlighted but I am doing something wrong when I replace the HTML content.
This is what the directive tries to do:
1.
The directive should highlight one or more words. For example.
If the search terms are "document legal" it should highlight both of them, even if they are not on this order.
So, a text like "legal something document" should get both highlighted, "legal" and "document".
2.
If the word is less than 3 characters is not going to get highlighted.
3.
If the word is not found, try removing the last character from it until its length is less than 3. You may search for "dimensions" and the search engine may return a text containing "dimension" or even "dime".
Just in case, the app is an Ionic App.
This is my code.
The angular directive:
angular.module('starter.directives', [])
.directive('highlightSearchTerms', function($compile) {
return {
restrict: 'A',
scope: true,
link: function($scope, element, attrs) {
$scope.highlightTerm = function(term) {
var html = element.html();
var highlighted = html.replace(new RegExp(term, 'gi'),
'<span class="highlightedText">$&</span>');
if (highlighted == null) {
return false;
}
// #see
// I think that the problem is here, it works the
// first time, but the second time it gets here
// the following exception is throwed
// "Cannot read property 'replaceChild' of null"
element.replaceWith(highlighted);
return html != highlighted;
};
var searchTerms = $scope.searchTerms;
if (searchTerms != undefined && searchTerms.length < 3) {
return;
}
var terms = searchTerms.split(' ');
// Try to highlight each term unless the word
// is less than 3 characters
for (var i = 0; i < terms.length; i++) {
var term = terms[i];
// // Try to highlight unless the word is less than 3 chars
while (term.length > 2) {
// If it got highlighted skip to next term
// else remove a character from the term and try again
if ($scope.highlightTerm(term)) {
break;
}
term = term.substring(0, term.length - 1);
}
}
}
};
});
You can see some weird things. Like using $scope.highlightTerm instead of passing the highlightTerm var to the directive. I couldn't get it work.
How can I change the HTML of the element correctly?
This is the template that is using the directive:
<div ng-include src="tplName" highlight-search-terms></div>
I wish to do something like that but I couldn't get it working:
<div ng-include src="tplName" highlight-search-terms="something to highlight"></div>
Here is a Plunker:
http://plnkr.co/edit/BUDzFaTnxTdKqK5JfH0U?p=preview
I think your code is working, but the issue was that you are trying to replace the whole div that is using the directive. So what you can do is just replace element.replaceWith(highlighted); with element.html(highlighted); and it will work.
I wish to do something like that but I couldn't get it working: <div
ng-include src="tplName" highlight-search-terms="something to
highlight"></div>
You already there, just use attrs in the link function like so:
var terms = attrs.highlightSearchTerms;, and you will get what you passed in highlight-search-terms="something to highlight"
This should work for you, with using of 'compile' function:
angular.module('starter.directives', [])
.directive('highlightSearchTerms', function($compile) {
return {
restrict: 'A',
scope: true,
compile: function(elem, attrs) {
// your code
elem[0].innerHTML = '<span class="highlightedText">$&</span>';
// your code
}
};
});
Documentation also could help.
Even tough punov's solution works, I think you shouldn't trigger multiple re-compiles for a single "line". I would suggest storing the html in a variable and recompile after every term was replaced.
Here is a working example - but it needs some polishing.
http://plnkr.co/edit/3zA54A0F2gmVhCComXAb?p=preview
link: function($scope, element, attrs) {
var searchTerms = $scope.searchTerms;
var terms = searchTerms.split(' ');
$scope.highlightedHTML = element.html();
if (searchTerms !== undefined && searchTerms.length < 3) {
return;
}
$scope.highlightTerm = function(term) {
console.log("html - ", term, html);
var highlighted = $scope.highlightedHTML.replace(new RegExp(term, 'gi'),
'<span class="highlightedText">$&</span>');
//element.replaceWith(highlighted);
return highlighted;
};
function highlight(terms, compile) {
// Try to highlight each term unless the word
// is less than 3 characters
// if the term is not highlighted remove one character
// from it and try again
for (var i = 0; i < terms.length; i++) {
var term = terms[i];
while (term.length > 2) {
var current = $scope.highlightedHTML;
$scope.highlightedHTML = $scope.highlightTerm(term);
if (current !== $scope.highlightedHTML) {
break;
}
term = term.substring(0, term.length - 1);
}
}
compile();
}
highlight(terms, function() {
element.replaceWith( $scope.highlightedHTML);
});
}
Using the common 'if ID exist' method found here, is it still possible check for the existence of the ID when concating the ID with an array variable like below?
for (var i=0; i < lineData.length; i++)
{
optionData = lineData[i].split(",");
if ($("#" + optionData[0]).length)
{
$("#" + optionData[0]).text(optionData[1]);
}
}
When running this in debugging, if the concated $("#" + optionData[0]) ID doesn't exist it yeilds a result of 'undefined: undefined' and jumps to:
Sizzle.error = function( msg ) {
throw "Syntax error, unrecognized expression: " + msg;
in the JQuery code.
Is it proper code etiquette to use check for, and set, HTML ID's in this manner? Why does this not work in the popular 'exist' method? What can I do to fix it and make it skip ID's that don't exist using this type of ID concatenation with an array string?
http://jsfiddle.net/P824r/ works fine, so the problem is not where you think it is. Simplify your code and add in some checks. You're also not doing anything that requires jQuery, so I don't see how this is a jQuery question, but fine:
function handler(data, i) {
var optionData = data.split(","),
$element;
if (optionData[0] && optionData[1]) {
$element = $("#" + optionData[0]);
if ($element.length > 0) {
// omitting >0 as 'trick' causes JS coercion from number to boolean.
// there's literally no reason to ever do so: it's both slower and
// hides your intention to others reading your code
$element.text(optionData[1]);
}
} else { console.error("unexpected optionData:", optionData);
}
lineData.forEach(handler);
but we can do this without jQuery, since we're not really using for anything that we can't already do with plain JS, in the same number of calls:
function handler(data) {
var optionData = data.split(",");
if (optionData.length === 2) {
var id = optionData[0],
content = optionData[1],
element = document.getElementById(id);
// because remember: ids are unique, we either get 0
// or 1 result. always. jQuery makes no sense here.
if (element) {
element.textContent = content;
}
} else { console.error("unexpected input:", optionData);
}
lineData.forEach(handler);
(the non-jquery version unpacks the optionsData into separate variables for improved legibility, but the ultimate legibility would be to make sure lineData doesn't contain strings, but just contains correctly keyed objects to begin with, so we can do a forEach(function(data) { ... use data.id and data.content straight up ... }))
If you want to keep this jQuery-related, there's more "syntax sugar" you're not making use of:
// check for ID in array
jQuery.each(someArray,
function(index, value) {
var the_id = $(value).attr('id');
if ( the_id !== undefined && the_id !== false ) {
// This item has an id
}
});
I have a set of strings and I need to find all all of the occurrences in an HTML document. Where the string occurs is important because I need to handle each case differently:
String is all or part of an attribute. e.g., the string is foo: <input value="foo"> -> Add class ATTR to the element.
String is the full text of an element. e.g., <button>foo</button> -> Add class TEXT to the element.
String is inline in the text of an element. e.g., <p>I love foo</p> -> Wrap the text in a span tag with class TEXT.
Also, I need to match the longest string first. e.g., if I have foo and foobar, then <p>I love foobar</p> should become <p>I love <span class="TEXT">foobar</span></p>, not <p>I love <span class="TEXT">foo</span>bar</p>.
The inline text is easy enough: Sort the strings descending by length and find and replace each in document.body.innerHTML with <span class="TEXT">$1</span>, although I'm not sure if that is the most efficient way to go.
For the attributes, I can do something like this:
sortedStrings.each(function(it) {
document.body.innerHTML.replace(new RegExp('(\S+?)="[^"]*'+escapeRegExChars(it)+'[^"]*"','g'),function(s,attr) {
$('[+attr+'*='+it+']').addClass('ATTR');
});
});
Again, that seems inefficient.
Lastly, for the full text elements, a depth first search of the document that compares the innerHTML to each string will work, but for a large number of strings, it seems very inefficient.
Any answer that offers performance improvements gets an upvote :)
EDIT: I went with a modification on Bob's answer. delim is an optional delimiter around the string (to differentiate it from normal text), and keys is the list of strings.
function dfs(iterator,scope) {
scope = scope || document.body;
$(scope).children().each(function() {
return dfs(iterator,this);
});
return iterator.call(scope);
}
var escapeChars = /['\/.*+?|()[\]{}\\]/g;
function safe(text) {
return text.replace(escapeChars, '\\$1');
}
function eachKey(iterator) {
var key, lit, i, len, exp;
for(i = 0, len = keys.length; i < len; i++) {
key = keys[i].trim();
lit = (delim + key + delim);
exp = new RegExp(delim + '(' + safe(key) + ')' + delim,'g');
iterator(key,lit,exp);
}
}
$(function() {
keys = keys.sort(function(a,b) {
return b.length - a.length;
});
dfs(function() {
var a, attr, html, val, el = $(this);
eachKey(function(key,lit,exp) {
// check attributes
for(a in el[0].attributes) {
attr = el[0].attributes[a].nodeName;
val = el.attr(attr);
if(exp.test(val)) {
el.addClass(attrClass);
el.attr(attr,val.replace(exp,"$1"));
}
}
// check all content
html = el.html().trim();
if(html === lit) {
el.addClass(theClass);
el.html(key); // remove delims
} else if(exp.test(html)) {
// check partial content
el.html(html.replace(exp,wrapper));
}
});
});
});
Under the assumption that the traversal is the most expensive operation, this seems optimal, although improvements are still welcome.
Trying to parse HTML with regex is a mug's game. It simply can't handle even the basic strucures of HTML, never mind the quirks. There's so much wrong with your snippet already. (Doesn't detect unquoted attributes; fails for a wide variety of punctuation in it due to lack of HTML-escaping, regex-escaping or CSS-escaping(*); failure for attributes with - in; strange non-use of replace...)
So, use the DOM. Yes, that'll mean a traversal. But then so does a selector like the [attr*=] you're using already.
var needle= 'foo';
$('*').each(function() {
var tag= this.tagName.toLowerCase();
if (tag==='script' || tag==='style' || tag==='textarea' || tag==='option') return;
// Find text in attribute values
//
for (var attri= this.attributes.length; attri-->0;)
if (this.attributes[attri].value.indexOf(needle)!==-1)
$(this).addClass('ATTR');
// Find text in child text nodes
//
for (var childi= this.childNodes.length; childi-->0;) {
var child= this.childNodes[childi];
if (child.nodeType!=3) continue;
// Sole text content of parent: add class directly to parent
//
if (child.data==needle && element.childNodes.length===1) {
$(this).addClass('TEXT');
break;
}
// Else find index of each occurence in text, and wrap each in span
//
var parts= child.data.split(needle);
for (var parti= parts.length; parti-->1;) {
var span= document.createElement('span');
span.className= 'TEXT';
var ix= child.data.length-parts[parti].length;
var trail= child.splitText(ix);
span.appendChild(child.splitText(ix-needle.length));
this.insertBefore(span, trail);
}
}
});
(The reverse-loops are necessary as this is a destructive iteration of content.)
(*: escape doesn't do any of those things. It's more like URL-encoding, but it's not really that either. It's almost always the wrong thing; avoid.)
There is really no good way to do this. Your last requirement makes you have to traverse the entire dom.
for the first 2 requirements i would select all elements by tag name, and interate over them inserting the stuff as needed.
only performance improvement i can think of is to do this on the server side at all costs, this may even mean an extra post to have your faster server do the work, otherwise this can be really slow on say, IE6
Given the following strings:
htmlStr1 = "<img>test1</img>";
htmlStr2 = "<div>test2</div>";
I'd like to know if there's a way to write a function just to detect for the "img" tag (for example). So if both of these strings are passed to it, and it should not do anything if the 2nd string is passed to it.
so for example, you'd run a function like:
result1 = checkIfTagExists(htmlStr1, "img");
result2 = checkIfTagExists(htmlStr2, "img");
alert(result1); // should output "true" or "1" or whatever
alert(result2); // should output "false" or do nothing
I would use a speedy RegExp for this, no need to use any jQuery selectors when not needed.
function checkIfTagExists(str, tag) {
return new RegExp('<'+tag+'[^>]*>','g').test(str);
}
If this is more of an example of functionality you are looking for and not the exact situation you'd use it in, the jQuery has selector may be helpful.
Related question with example.
For this situation you would do:
var str1ContainsImg = $(htmlStr1 + ":has(img)").length > 1;
var str2ContainsImg = $(htmlStr2 + ":has(img)").length > 1;
Edit: As tvanfosson pointed out in the comments, if your img tag doesn't have a closing tag ( <img src='' /> ),this exact solution wouldn't work. If that's an issue, you can check the tag name of the first element returned like this:
var htmlStr3 = "<img src='' />";
var containsEmptyImg = $(htmlStr3 + ":has(img)").length > 1 ||
$(htmlStr3 + ":has(img)")[0].tagName.toUpperCase() == 'IMG';
Wrap this in an if statement and you are good to go
jQuery(jQuery(htmlStr1)).find('img').size() > 0
This will be easier if the strings are in the innerHTML of an element:
function checkIfTagExists(element, tagName) {
return element.getElementsByTagName(tagName).length > 0;
}
A regex check would probably be insufficient (see RegEx match open tags except XHTML self-contained tags) :)
If you really do just have the strings, you can create an element from the string, and set the innerHTML to the string, then do the above check on that new element. This is what the $(tagName, htmlContext) would be doing.
This might have a bit of overhead, but it should be robust enough for use with any given string or the innerHTML of any element.
function getTagCountFromString(string, tag) {
var count = 0;
tag = tag.toLowerCase();
$(string).each(function(idx, el){
if(el.nodeType === 1) {
if(el.tagName && el.tagName.toLowerCase() === tag) {
count++;
}
if(el.childNodes.length > 0) {
try {
count += getTagCountFromString(el.innerHTML, tag);
} catch (ex) { }
}
}
});
return count;
}
getTagCountFromString('<img src=""/><a href=""/>', 'img'); //returns 1
Then to get a boolean value you could check if the count is not equal to 0 or make a helper function which does it for you.
in straight javascript:
function checkIfTagExists(src, tag) {
var re = "<"+tag + ">\.+<\\/"+tag+">";
return new RegExp(re).test(src);
}