I'm making a webapp that will search cards based on parameters given.
The problem lies on the fact that the cards have different types. For instance one card may have something like "income": 4, while another behaves completely different and has an attribute "cost" : 5. All i meant is that my JSON has 5 types of different objects, that although they share many similarities, they have certain distinctions.
So let's say that my user searches for a card based on the Title. All of them has this attribute, so it's fairly easy to make an condition like this.
if ((item.name.toLowerCase().indexOf(cardParams.searchTitle.toLowerCase()) > -1) && (cardParams.searchTitle != null ) && (cardParams
.searchTitle.length > 0)){
But what if my user wants to search the text in the body too? All of them the have one too, so this leads to another condition with starts making things awkward.
Furthermore, let's way that my user triggers my worst case scenario and asks for two attributes that no card has them at the same time along with the ones above. One would say that i should work on my form to make them mutually exclusive, but programmatically this is beyond me.
What i've decided to go for is to search for each attribute seperately and save the results all into an array. If i have as many duplicates of an card as many valid attributes (not null or empty), then i would keep them.
This is really really clumsy and not optimal. I'm sure there are very simple solutions that i can't think of.
How can i proceed such a problem? Is creating tons of conditions the only way to go? What should i do?
Edit: Thanks for the downvotes. It's like i ask for fun.
If you want to search across an object inside javascript, you can just loop over the properties, and access those for which the object.hasOwnProperty(propertyName) returns true
As an example, you see here a bunch of cards, with different properties, the search will however iterate over all of the properties of each card and will select a card that matches and then search for the next matching card.
At the end it will either show the results, or simply say there are no results found ;)
var cards = [{
title: 'Card 1',
prop1: 'Test of card1',
description: 'Description of card1',
customCard1: 'Custom property'
}, {
title: 'Card 2',
description: 'Description of card2',
customCard2: 'Custom property card2'
}];
function searchCards() {
var txt = document.getElementById('searchBox').value.toLowerCase(),
i, len, card, prop, matches = [],
val;
for (i = 0, len = cards.length; i < len; i++) {
card = cards[i];
for (prop in card) {
if (card.hasOwnProperty(prop)) {
val = card[prop];
if (typeof val !== 'undefined' && val.toLowerCase && val.toLowerCase().indexOf(txt) >= 0) {
// this card matches
matches.push(card);
break;
}
}
}
}
showMatches(matches);
}
function showMatches(matches) {
var elem = document.getElementById('result'),
i, len, content = '';
if (typeof matches === 'undefined' || !matches.length) {
elem.innerHTML = '<i>No results found</i>';
return;
}
for (i = 0, len = matches.length; i < len; i++) {
content += '<div><b>title:</b>' + matches[i].title + '</div>';
}
elem.innerHTML = content;
}
<input type="text" id="searchBox" />
<button type="button" onclick="javascript:searchCards()">Search</button>
<div id="result"></div>
Update based on the comments
If you have multiple input fields, you could give them an extra class to recognise them, and then let jQuery return you all the matching elements based on your selector (in my case, .search), and get the values that were given by the user, then you can simply check if your card has a defined value for it, and if this defined value is really part of the card itself
I reused some code from the first snippet, didn't want to spend to much extra time on it ;)
var cards = [{
title: 'Card 1',
prop1: 'Test of card1',
description: 'Description of card1',
customCard1: 'Custom property'
}, {
title: 'Card 2',
description: 'Description of card2',
customCard2: 'Custom property card2'
}];
function getSearchParameters() {
var properties = [];
$('.search').each(function() {
if (this.value) {
properties.push(this.value);
}
});
return properties;
}
function search() {
var properties = getSearchParameters(), i, len, card, matches = [], p, plen, prop, match;
for (i = 0, len = cards.length; i < len; i++) {
card = cards[i];
match = true;
for (p = 0, plen = properties.length; p < plen; p++) {
prop = properties[p];
if (typeof card[prop] === 'undefined' || !card.hasOwnProperty(prop) ) {
// didn't find a property, this doesn't match
match = false;
break;
}
}
if (match) {
matches.push(card);
}
}
showMatches(matches);
}
function showMatches(matches) {
var elem = document.getElementById('result'),
i, len, content = '';
if (typeof matches === 'undefined' || !matches.length) {
elem.innerHTML = '<i>No results found</i>';
return;
}
for (i = 0, len = matches.length; i < len; i++) {
content += '<div><b>title:</b>' + matches[i].title + '</div>';
}
elem.innerHTML = content;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<input type="text" class="search" />
<input type="text" class="search" />
<input type="text" class="search" />
<input type="text" class="search" />
<input type="text" class="search" />
<input type="text" class="search" />
<input type="text" class="search" />
<input type="text" class="search" />
<button type="button" onclick="search()">Search for matching properties</button>
<div id="result">
</div>
Related
There is an object with keys whose values are text, and this text should appear in the placeholder of the search page on the site. The first key is written one letter at a time until the whole word is created, and then, one letter at a time, it is completely deleted, and the second key is written in the same way in placeholder. The dialing speed does not matter.
I am enclosing my work. Now the problem is that all the keys are written, but they must take turns, deleting the previous one by letter.
let types={
phrase1:"Words",
phrase2:"has been",
phrase3:"deleted",
};
function writer() {
let curr = 0;
let text = Object.values(types)
let elem = document.getElementsByClassName('topnav');
elem.textContent += text.charAt(curr);
curr++
if (curr < text.length )
window.setInterval(writer,60);
}
writer();
<div class="topnav">
<input type="text" placeholder=" ">
</div>
Solution
call setTimeout() because it will call the method once whereas setIntervall() will call it repetitive what doesn't make sense here because you will call it in your next function call again.
use recursion to get the next word of your array
Defined three cases here
current letter < wordsArray.length apply method on the same element of the array
When 1 is false and value < wordsArray apply method on the next array element writer(value+1)
1 and 2 are false then print out the content of the array as string
let types = {
phrase1: "Words",
phrase2: "has been",
phrase3: "deleted",
};
var curr = 0;
function writer(value) {
let inp = document.getElementById("text");
let wordArray = Object.values(types)
let sentence = wordArray[value];
inp.placeholder = sentence.charAt(curr);
curr++;
if (curr < sentence.length){
window.setTimeout(function() {
writer(value);
}, 500);
}else if(value < wordArray.length-1){
window.setTimeout(function() {
curr = 0;
inp.placeholder = " ";
writer(value+1);
}, 500)
}else {
inp.placeholder = wordArray.join().replaceAll(",", " ");
}
}
writer(0);
<div class="topnav">
<input id="text" type="text" placeholder=" ">
</div>
I've made a demo, from the very little you could piece together from your question. You really didn't specify anything... BUT I would like to add that the types variable should be an Array and not an Object which makes it much easier to work with. Nonetheless, I did work with your provided code:
const types = {
phrase1:"Words",
phrase2:"has been",
phrase3:"deleted",
}
const inputElm = document.querySelector('.topnav > input')
function iteratePlaceholder(place = 1) {
inputElm.placeholder = types[`phrase${place||1}`];
// call this function again only if "place" is less than the length of keys (3)
(place < Object.keys(types).length) && setTimeout(iteratePlaceholder, 500, ++place)
}
iteratePlaceholder()
<div class="topnav">
<input type="text" placeholder=" ">
</div>
To start off, I'm primarily an AngularJS developer and recently switched to React, and I decided to convert an angular webapp I had previously developed to a react app. Im having a bit of an issue with a component ExpressiveText that searches through a string for a match to a property on a list objects and inserts a component TriggerModal in its place that when clicked triggers a modal with more detailed information. So the properties passed into ExpressiveTest are: text, tags, and tagsProperty.
text is a string (i.e. "My search string")
tags is an array of objects (i.e. [{id: 1, name: 'my', data: {...}}, {id: 2, name: 'string', data: {...}}]
tagsProperty is the name of the property to search for as a "tag" (i.e. name)
I followed along with this issue to try and formulate an idea of how to approach this. The reason I mention that I am coming from angular is because the component I had previously created simply used something like text.replace(regex, match => <trigger-modal data={tags[i]} />) and then used angulars $compile function to render components in the text. This does not seem to be possible using react. This is what I have tried inside of my ExpressiveText component:
class ExpressiveTextComponent extends React.Component {
constructor (props) {
super(props);
this.filterText = this.filterText.bind(this);
}
filterText () {
let text = this.props.text;
this.props.tags.map(tag => {
const regex = new RegExp(`(${tag[this.props.tagsProperty]})`, 'gi');
let temp = text.split(regex);
for(let i = 1; i < temp.length; i+=2){
temp[i] = <TriggerModal data={tag} label={tag[this.props.tagsProperty]} />;
}
text = temp;
});
return text;
}
render () {
return (
<div className={this.props.className}>{this.filterText()}</div>
);
}
}
This works for the first tag. The issue with it is that once it goes to map on the second tag, text is then an array. I tried adding in a conditional to check if text is an array, but then the issue becomes that the text array becomes nested and doesnt work on the next iteration. Im having a really hard time wrapping my mind around how to handle this. I have also tried dangerouslySetInnerHTML using text.replace(...) but that doesn't work either and just renders [object Object] in place of the component. Any help or advice is much appreciated, I have to say this is probably the only major issue I have come across since my switch to React, otherwise its been very straightforward.
Edit: Since I had a question asking for expected output with a given input and more clarification, what I am looking for is a component that is given this input:
<ExpressiveText text="my text" tags={{id: 1, name: 'text'}} tagsProperty="name" />
would render
<div>my <TriggerModal label="text" data={...} /></div>
with a functional TriggerModal component.
If I am correct in my understanding of what you're trying to accomplish, this is one way to do this it. My apologies if I misunderstood your question. Also, this is pseudocode and I'll try and fill it in with real code in a bit. Sorry if this is difficult to understand, let me know and I will try to clarify
filterText () {
let text = [this.props.text];
for (let item in this.props.tags) {
//item will be something like {id: 1, name: 'text'}
let searchString = new RegExp(item.name, 'gi');
//loop through text array and see if any item matches search string regex.
while (text.some(val => val.test(searchString)) {
//if we are here, at least one item matches the regexp
//loop thru text array, and split any string by searchString, and insert <TriggerModal> in their place
for (let i = text.length-1; i >=0; i--) {
//if text[i] is string and it matches regexp, then replace with nothing
text[i].replace(searchString, "")
//insert <trigger modal>
text.splice(i, 0, <TriggerModal ... />)
}
//end of while loop - test again to see if search string still exists in test array
}
}
return text;
}
Looks like I found a solution.
filterText () {
let text = this.props.text.split(' '),
replaceIndexes = [];
if(this.props.tags.length > 0) {
this.props.tags.map(tag => {
const regex = new RegExp('(' + tag[this.props.tagsProperty] + ')', 'gi');
for(let i = 0; i < text.length; i++){
if(text[i].match(regex)){
/**
* Pretty simple if its a one-word tag, search for the word and replace.
* could potentially cause some mis-matched tags but the words
* in my usecase are pretty specific, unlikely to be used in
* normal dialogue.
*/
text[i] = <TriggerModal data={tag} label={tag[this.props.tagsLabelProperty || 'name']} />;
}else{
// for tags with spaces, split them up.
let tempTag = tag[this.props.tagsProperty].split(' ');
// check for length
if(tempTag.length > 1) {
// we will be replacing at least 1 item in the array
let replaceCount = 0,
startIndex = null;
// If the first word of tempTag matches the current index, loop through the rest of the tempTag and check to see if the next words in the text array match
if(tempTag[0].toLowerCase() === text[i].toLowerCase()){
startIndex = i;
replaceCount += 1;
// loop through temp array
for (let j = 0; j < tempTag.length; j++) {
if(tempTag[j].toLowerCase() === text[i+j].toLowerCase()){
replaceCount += 1;
}
}
// Push data into replaceIndexes array to process later to prevent errors with adjusting the indexes of the text object while looping
replaceIndexes.push({
startIndex: startIndex,
replaceCount: replaceCount,
element: <TriggerModal data={tag} label={tag[this.props.tagsLabelProperty || 'name']} />
});
}
}
}
}
});
}
// Loop through each replace index object
replaceIndexes.forEach((rep, index) => {
text.splice(rep.startIndex - index, rep.replaceCount, [rep.element, ', ']);
});
// Since we stripped out spaces, we need to put them back in the places that need them.
return text.map(item => {
if(typeof item === "string"){
return item + ' ';
}
return item;
});
}
Edit: This is actually pretty buggy. I ended up ditching my own solution in favor of this package
I have a small input field where this code gets activated everytime a key is pressed inside it. But it now only prints "found something" when the name exacly matches what you type in the input field.
How can change a part that when I type something like "b" it already removes the matches where there is no "b" in the name is and print every possible matches that still have a "b".
My small code to find the match.
Info is my json big array where I can loop through all the names with info[i].name
var textInput = $findperson.find('input').val();
console.log(textInput);
for (i = 1; i < info.length; i++) {
if (textInput === info[i].name) {
console.log('found something');
}
}
Set Flag if found any match and print them, otherwise print found nothing,
for gi g mean search globally and i mean ignore case sothat A will match a and vise verse.
var textInput = $findperson.find('input').val();
console.log(textInput);
found = false
for (i = 1; i < info.length; i++) {
if (info[i].name.match(new RegExp(textInput,"gi")) ) {
console.log(info[i].name);
found = true
}
}
if(!found){
console.log("found nothing")
}
I would use regex like this:
var textInput = $findperson.find('input').val();
var regex = new Regexp(".*("+textInput+").*","i");
var filtered = info.filter(function (current) {
return current.name.match(regex);
});
console.log(filtered);
Just use indexOf to search for one String within another:
if(info[i].name.indexOf(textInput) != -1) {
indexOf will return -1 if String isn't found within the other.
You can try searching for some letters in one of the results 'balloon', 'ball', 'apple' in the example below:
var results = ['balloon', 'ball', 'apple'];
function filterResults() {
var input = document.getElementById('input').value;
var resultsFiltered = results.filter(function(a) {
return a.indexOf(input) != -1;
});
var result = ''; resultsFiltered.map(function(a) {
result += a + '<br/>';
}); document.getElementById('result').innerHTML = result;
}
<input id='input' onkeyup='filterResults();'/>
<div id='result'></div>
My webpage has a set of divs overlapping each other and but with different z-index values. I am trying to implement a text search functionality so that when the text is found in a div, I can bring that div to the foreground. Something like flipping the pages of a book to the page containing the text.
One way to do this would be to loop through the innerHTMLs of all the text DOM elements in each div and then using the xpath of each resultant element to determine which div it belongs to and bringing that div to the foreground. But it would be highly inefficient.
Rather I am wondering if there is some way to delegate this task to the browser itself. I tried text searching in Chrome, firefox, safari etc.. They found the text even in the hidden divs. Is there any way I can refer to those search results to determine the xpath of the results?
Basically do browsers provide us with any APIs that can help me?
Thanx in advance!!
Note: I am looking for a pure javascript solution or at best I can use google closure.
I rustled up an approach I would take to doing a text search.
The basic idea would be that you do the searching without touching the dom, therefore saving quite a bit of processing overhead.
http://jsfiddle.net/j84aX/2/
Basically this is what my example does:
//Data comes in the form of an array of objects.
var results = [{
id: Math.floor(Math.random() * 10000),
title: 'Some sort of title'
}, {
id: Math.floor(Math.random() * 10000),
title: 'Some other item'
}, {
id: Math.floor(Math.random() * 10000),
title: 'Maybe a thingy'
}]
// Templates are created and references attached to the original objects
// Templates are attached to the dom
renderResults = function() {
var i = 0,
len = results.length,
item;
for (; i < len; i++) {
item = $('<li data-id="'+ results[i].id +'">'+ results[i].title +'</li>');
results[i].item = item;
item.appendTo(resultsEl);
}
$('body').append(resultsEl);
}
// A user searches and a lookup is done in the result set
$('.search input').on('keyup', function() {
});
// Any matches already have the item on their object and do not need to do a dom lookup
// Matches are highlighted using a class name
for (; i < len; i++) {
if (results[i].title.toLowerCase().indexOf(val) !== -1) {
results[i].item.addClass('match')
}
}
You can use jQuery plugin: Search engine:
$('div').searchengine(myText,'html',false);
All divs selected
myText : text that I search on Divs
'html' : Using html method, it can be 'val' method if its input or 'text' method if its pre ,...etc
Adding this plugin after loading jQuery
$.fn.searchengine = function(text, action, casesensitive, exactText) {
var arrayJq = []
var all = $(this);
all.each(function() {
var html = $(this).html();
var vll = $(this).val();
var ttx = $(this).text();
if (casesensitive === false) {
html = html.toLocaleLowerCase();
vll = vll.toLocaleLowerCase();
ttx = ttx.toLocaleLowerCase();
text = text.toLocaleLowerCase();
}
var test;
if (exactText = true) {
test = (html === text) || (vll === text) || (ttx === text)
} else {
test = (html.indexOf(text) !== -1)
|| (vll.indexOf(text) !== -1)
|| (ttx.indexOf(text) !== -1)
}
if (test === true) {
arrayJq.push($(this));
if (action.length !== 0) {
$(this)[action]();
}
}
})
return arrayJq;
};
Update:
To get th xpath, you can use fullselector jQuery plugin :
$('div#myDiv').fullselector();
known that you must add this code after jQuery loading :
$.fn.id=function(){
var arr=[];
if($(this).length>0){
arr.push($(this).get(0).tagName.toLocaleLowerCase());
$(this).parents().each(function(i,e){
var ind=$(e).index();
ind='['+ind+']';
arr.push($(e).get(0).tagName.toLocaleLowerCase()+ind)});
arr.reverse();
}
return arr.join('//');
};
/**
* Full jquery selector of element
* #depend of $.id() jquery plugi
*/
$.fn.fullselector=function(){
var arr=[];
if($(this).get(0)){
arr.push($(this).get(0).tagName.toLocaleLowerCase());
}
$(this).parents().each(function(i,e){
//var ind=mthpathid.indexChildsametag($(e).parent(),$(e))
var ind=$(e).index();
ind=':eq('+ind+')';
if($(e).get(0).tagName.toLocaleLowerCase()=='html'){
arr.push($(e).get(0).tagName.toLocaleLowerCase()+ind)
}else{
arr.push(ind)
}
});arr.reverse();
return arr.join('>');
};
var mthpathid={
indexChildsametag:function(jqparent,jqchild){
var j=-1;
jqparent.children(jqchild.get(0).tagName.toLocaleLowerCase()).each(function(i,e){
if($(e).id()===jqchild.id()){
j=i;
}
});
return j;
}
};
I'm using multiple checkboxes to filter properties using angularjs. Currently, I am using a custom filter to show all properties of a certain type. I have multiple checkboxes that as you check each new one it filters the results.
At present, each new checkbox you check narrows your search (i.e. which properties are both rural AND coastal) and I would like to widen the search (i.e. which properties are either rural OR coastal). I'm really new to this.
Here is my app:
propertyApp.controller('PropertyListControl', function ($scope) {
$scope.properties = [
{
title: "Sharrow Bay Hotel",
location:['rural', 'coastal']
},
{
title: "The Royal Oak Inn",
location:['rural']
},
{
title: "Scale Hill Cottages",
location:['urban']
},
];
$location = {}
// Currently using this great custom filter:
}).filter('filteredLocation', function() {
return function(properties, location) {
var result = properties.slice(); // copy array
angular.forEach(location, function(value, key) {
if(value) {
for(var index = 0; index < result.length; index++) {
property = result[index];
if(property.location.indexOf(key) == -1) {
result.splice(index--,1);
}
}
}
});
return result;
};
});
And my checkboxes:
<label><input type="checkbox" ng-model="location.rural"/>Rural</label>
<label><input type="checkbox" ng-model="location.urban"/>Urban</label>
<label><input type="checkbox" ng-model="location.coastal"/>Coastal</label>
That filter starts with all your locations:
var result = properties.slice();
and removes any that don't match your test:
result.splice(index--,1);
Thus it's acting like an "and" since, as in your example, anything without "coastal" is removed and then anything without "Rural" is removed. So the only items left are ones that match both conditions.
To turn it into an "or" filter I'd start with an empty array:
var result = [];
and add the results as they match (so any that match either test will be added):
result.push(property);
To avoid duplicates I've also switched the loops so the outer loop now covers the list of properties and the inner loop goes over the list of locations to filter. Then we can abort out of the inner loop once we find that the property matches any of the locations.
Here's the entire function:
.filter('filteredLocation', function() {
return function(properties, location) {
var result = [];
for(var index = 0; index < properties.length; index++) {
var added = false;
angular.forEach(location, function(value, key) {
if(value && !added) {
property = properties[index];
if(property.location.indexOf(key) != -1) {
result.push(property);
added = true; // Mark as added so we don't add duplicates
}
}
})
};
return result;
};
demo fiddle