I have to list of words:
<div class="first-word-list">
<span>apple</span>
<span>sea</span>
<span>ship</span>
</div>
<div class="second-word-list">
<span>duck</span>
<span>tale</span>
<span>jewelry</span>
</div>
the client can add any word to these list so it's not only going to be these 6 words.
I'm using a function that when on click event, it changes the word creating a combination between these two lists on this structure:
<div class="first-word"></div><div class="second-word"></div>
$('.first-word').click(function(){
$('.first-word').text($(".first-word-list span").eq(Math.floor(Math.random()*firstWordLength)).text());
});
$('.second-word').click(function(){
$('.second-word').text($(".second-word-list span").eq(Math.floor(Math.random()*firstWordLength)).text());
});
So, if the user clicks on .first-word and on .second-word it displays this:
<div class="first-word">sea</div><div class="second-word">tale</div>
but lets say I want to block/avoid to display the combination of ship and jewelry. Since I'm using a random function and like I wrote before, the word list will grow I need a way to control which words combinations won't be shown. Any idea how to do this?
Update
The client will write the banned combinations on two new custom fields which will output on a new list like this:
<div class="banned-combinations">
<div><span>ship</span><span>jewelry</span></div>
<div><span>apple</span><span>duck</span></div>
</div>
Here is a snippet that allows you to dynamically add words to either list, and to select words from those lists in order to mark them as forbidden combinations. Once you have everything entered as you want to have it, you can click the two random words to have them replaced by a new random word (which could be just the same word again), taking into account the forbidden combinations:
// Allow adding words to either word list:
$(".add-word").on("change keydown", function (e) {
if (e.which && e.which != 13) return;
$(this).before($("<span>").text($(this).val()));
$(this).val('');
});
// Allow selecting words by clicking them
$(".word-list").on("click", "span", function () {
$(".selected", $(this).parent()).removeClass("selected");
$(this).addClass("selected");
showButton();
});
// Allow forbidding pairs of selected words:
$("#forbid").click(function () {
$(".forbidden").append(
$("<div>").append(
$(".selected").removeClass("selected").clone(),
$("<button>").addClass("del").text("Del")
)
);
showButton();
});
// Remove forbidden pair:
$(document).on("click", ".del", function () {
$(this).parent().remove();
});
// Hide/show button depending on whether we have enough selected words:
function showButton() {
$("#forbid").toggle($(".selected").length == 2);
}
// Main algorithm:
$(".random-word").click(function () {
var index = $(this).index(".random-word");
// Get word that is not clicked:
var otherword = $(".random-word").eq(1-index).text();
// Get all words from corresponding list
var words = $("span", $(".word-list").eq(index)).map(function () {
return $(this).text();
}).get();
// Get list of forbidden words that are linked with the other (non-clicked) word
var forbidden = $('.forbidden div').filter(function () {
return $('span', this).eq(1-index).text() == otherword;
}).map(function () {
return $('span', this).eq(index).text();
}).get();
// Derive the list of allowed words, taking all words, filtering out what is forbidden
var allowed = words.filter(function (txt) {
return forbidden.indexOf(txt) == -1;
});
// Pick a random from these allowed words:
$(this).text(allowed.length
? allowed[Math.floor(Math.random() * allowed.length)]
: "(none)"
);
});
showButton();
// *** Upon special request following was added ***
// Algorithm for choosing random pair in one go:
$("#choose").click(function () {
// Get all words from both lists
var words = $(".word-list").get().map(function (list) {
return $("span", list).get().map(function (span) {
return $(span).text();
});
});
// Get list of forbidden words pairs
var forbidden = $('.forbidden div').get().map(function (div) {
return $('span', div).get().map(function (span) {
return $(span).text();
});
});
// Derive the list of allowed pairs, taking all pairs, filtering out what is forbidden
var allowed = words[0].reduce(function (pairs, word) {
// Get list of forbidden second words, given the first word
var exclude = forbidden.filter(function(pair) {
return pair[0] == word;
}).map(function (pair) {
return pair[1]; // extract second word of the pair
});
// Filter all second words, excluding those that are forbidden pairs with first word.
return pairs.concat(words[1].filter(function (word2) {
return exclude.indexOf(word2) == -1;
}).map(function (word2) {
return [word, word2];
}));
}, []);
// Pick a random pair from these allowed pairs:
var randomPair = allowed.length
? allowed[Math.floor(Math.random() * allowed.length)]
: ["(none)", "(none)"];
// Display the pair
$(".random-word").each(function (i) {
$(this).text(randomPair[i]);
});
});
span {margin-left: 2px; margin-right: 2px}
span.selected {background-color: yellow}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<b>First word list:</b>
<div class="word-list">
<span>apple</span>
<span>sea</span>
<span>ship</span>
<input class="add-word" size=10>
</div>
<b>Second word list:</b>
<div class="word-list">
<span>duck</span>
<span>tale</span>
<span>jewelry</span>
<input class="add-word" size=10>
</div>
<button id="forbid">Add combination as forbidden</button><br>
<b>Forbidden combinations:</b>
<div class="forbidden">
</div>
<b>Random Pair (click a word to reselect a random, allowed word):</b>
<div class="random-word">sea</div><div class="random-word">tale</div>
<button id="choose">Random Pick Both</button>
I'm not sure how you want to handle adding banned options but if everything needs to be dynamic then you could always utilize a dictionary (object).
var bannedOptions = {
'ship': {
'jewelry': true
}
};
When you go to check if an option is valid you can simply do something like:
// Here I'm using truthy checks,you can always add a != null
// if you want false or something to be a valid value
if(bannedOptions[word1] && bannedOptions[word1][word2]) {
// It's invalid
} else {
// Valid
}
When you want to add a new invalid option you could
if(!bannedOptions[newWord]) bannedOptions[newWord] = {};
bannedOptions[newWord][invalidWord] = true;
If you wanted to remove an invalid option you could do:
bannedOptions[word1][word2] = false;
or
delete bannedOptions[word1][word2];
Using a dictionary allows you to have super fast checks when you want to see if a combination is invalid.
It's super dynamic so you don't have to worry about indices or anything because the words themselves are keys... and as long as the word is accessed as a string you can have symbols like -,',/, etc. in the words.
Related
I have written a javaScript file in jQuery that provides a search function. I am trying to figure out how to highlight the word aswell. Bellow is the code.
Filter.js:
(function ($) {
// custom css expression for a case-insensitive contains()
jQuery.expr[":"].Contains = jQuery.expr.createPseudo(function(arg) {
return function( elem ) {
return jQuery(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0;
};
});
function listFilter(header, list, title) {
// header is any element, list is an unordered list, title is any element
// create and add the filter form to the header
// create a button for collapse/expand to the title
var form = $("<form>").attr({"class":"filterform","action":"#"}),
button = $("<input>").attr({"class":"rest", "type":"submit", "value":"Collapse All", "id":"switch"}),
input = $("<input>").attr({"class":"filterinput","type":"text", "placeholder":"Search"});
$(form).append(input).appendTo(header); //add form to header
$(title).append(button); //add button to title
//on click function for collapse/expand all
$("#switch").click(function(){
if($(this).val() == "Collapse All"){
$(".filterinput").val("");
$(this).val("Expand All");
$("div.content div.markdown").parent().parentsUntil(list).hide();
$(list).find("span.path").parentsUntil(list).show();
$(list).find("ul.endpoints").css("display", "none");
}
else{
$(".filterinput").val("");
$(this).val("Collapse All");
$("div.content div.markdown").parent().parentsUntil(list).hide();
$(list).find("span.path").parentsUntil(list).show();
}
});
$(input)
.change( function () {
var filter = $(this).val();
if(filter) {
// this finds a single string literal in div.markdown,
// and hides the ones not containing the input while showing the ones that do
$(list).find("div.content div.markdown:not(:Contains(" + filter + "))").parent().parentsUntil(list).hide();
$(list).find("div.content div.markdown:Contains(" + filter + ")").parent().parentsUntil(list).show();
}
else {
$("div.content div.markdown").parent().parentsUntil(list).hide();
$(list).find("span.path").parentsUntil(list).show();
$(list).find("ul.endpoints").css("display", "none");
}
return false;
})
.keyup( function () {
// fire the above change event after every letter
$(this).change();
});
}
//ondomready
setTimeout(function () {
listFilter($("#header"), $("#resources"), $("#api_info"));
}, 250);
}(jQuery));
The html that I would like to manipulate is being dynamically created by another JS file so I need to manipulate the DOM after it has been completely rendered.. The html that I will be focusing on gets rendered as bellow, specifially the words in (div class="markdown").
Index.html:
<div class="content" id="connectivitypacks_get_connectivitypacks_content">
<h4>Description</h4>
<div class="markdown"><p>Response will return details for the connectivity packs based on the ID.</p>
<h2 id="keywords">Keywords</h2>
<p> foo, bar, helloWorld, java</p>
</div>
</div>
Here is an example that used your markdown.
Create a regex with that word your searching for.
Get the html of your .markdown
replace the word with <span class="marker">"+ word +"</span>. So this creates a span tag around the word your searching for.
Create css to style the word as needed.
function highlight(word) {
var element = $('.markdown');
var rgxp = new RegExp(word, 'g');
var repl = '<span class="marker">' + word + '</span>';
element.html(element.html().replace(word, repl));
}
highlight('details');
.marker {
background-color: yellow;
font-weight: bold;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="content" id="connectivitypacks_get_connectivitypacks_content">
<h4>Description</h4>
<div class="markdown">
<p>Response will return details for the connectivity packs based on the ID.</p>
<h2 id="keywords">Keywords</h2>
<p>foo, bar, helloWorld, java</p>
</div>
</div>
Have a look at mark.js. It can highlight such search terms in a specific context. In your example the JavaScript would look like:
var searchTerm = $("#theInput").val();
// Search for the search term in your context
$("div.markdown").mark(searchTerm, {
"element": "span",
"className": "highlight"
});
and the CSS part:
span.highlight{
background: yellow;
}
I have html page containing 70 or so divs, all with the same name, but each has different content within it. I am trying to make them searchable by filtering the content using jquery. I have it working already somewhat, you can see the page here:
http://smokefreehousingak.org/find-housing.html
The trouble I'm having is combining both the input from text, and the selects you see underneath the search bar. Right now the divs are filtered, but only per item, that is you cannot put a string in the text input, then change the value of the selects, and have it filter the divs based on all 3 or 4 pieces of data. It will only filter based on whichever input or select was last acted upon.
The jquery/javascript for the input filtering is thus:
function searchPage(searchQuery) {
$('.secondary-container').hide();
var searchQuery = $('#search-criteria').val();
console.log("search query is");
console.log(searchQuery);
$('.secondary-container').each(function(){
if($(this).text().toUpperCase().indexOf(searchQuery.toUpperCase()) != -1){
$(this).fadeIn(450);
}
});
console.log("page searched");
}
$('#search').click(function(){
searchPage();
});
$("#searchbox").submit(function() {
searchPage();
return false;
});
The HTML for each item being filtered is like so (just with different info in each one):
<div class="main-container secondary-container">
<aside id="filler-aside">
</aside>
<div class="main-content full listing">
<div class="main-content listing-picture" style="background: url('img/maps/image70.jpg') no-repeat center center;-webkit-background-size: cover;-moz-background-size: cover;-o-background-size: cover;background-size: cover;"></div>
<div class="main-content listing-data">
<h3>"Retirement Community of Fairbanks Inc."</h3>
<h3>Raven Landing</h3>
<p><span>Address:</span> 1222 Cowles St.<br>
<span>City:</span> Fairbanks<br>
<span>Zip:</span> 99701<br>
<span>Type of Housing:</span> Senior <br>
<span>Classification:</span> * Smoking is not allowed anywhere inside a building or complex</p>
</div>
</div>
</div>
The jquery/javascript for filtering divs based on one a select is like so (this is the code for the city select):
function citySelect() {
$('.secondary-container').hide();
$("#city-select").bind("change", function() {
console.log("city changed to");
city = $("#city-select option:selected").text();
console.log(city);
// searchPage(city);
$('.secondary-container').each(function(){
if($(this).text().toUpperCase().indexOf(city.toUpperCase()) != -1){
$(this).fadeIn(450);
console.log("text");
}
});
});
Right now I have each select with its own function thats called after its acted on, that then filters the divs based on the data selected. I think what I need is just one function that gets called anytime a select or input is acted upon, that will filter the divs based on all inputs or selects instead of statically choosing just one piece of data to work with.
Currently, the input search filter works by seeing if any div contains the text inputed into the field:
$('.secondary-container').each(function(){
if($(this).text().toUpperCase().indexOf(searchQuery.toUpperCase()) != -1){
$(this).fadeIn(450);
}
});
I need to somehow say if it includes searchQuery AND input data, etc... same thing when the inputs, I need basically this whole search function to act on all the data input not just one piece. Any suggestions?
You can do something like this:
function searchPage() {
var search = $('#search-criteria').val().toUpperCase();
var city = $("#city-select option:selected").text().toUpperCase();
$('.secondary-container').each(function(){
var text = $(this).text().toUpperCase();
$(this).hide();
if((!search || text.indexOf(search) !== -1) && (!city || text.indexOf(city) !== -1)) {
$(this).fadeIn(450);
}
});
};
I also added !searchand !city to make sure the result is shown if the string is empty.
Since the above ignores the selects if they are empty, but initially they have a default value of "Filter By...", I made a function that checks if the value of the select is "Filter By..." and if so, it sets the city variable to being empty, so that it will get ignored (via !search and !city), and if the value has been changed, then it executes how it normally would.
var search = $('#search-criteria').val().toUpperCase();
var city = $("#city-select option:selected").text().toUpperCase();
var cityStart = "Filter By City";
if(city.indexOf(cityStart.toUpperCase()) != -1){
console.log("city untouched");
console.log(cityStart);
var city = "";
console.log(city);
} else {
console.log("city not default");
}
$('.secondary-container').each(function(){
var text = $(this).text().toUpperCase();
$(this).hide();
if((!search || text.indexOf(search) !== -1) && (!city || text.indexOf(city) !== -1)) {
$(this).fadeIn(450);
}
});
I'm looking for tutorial or example on how to implement a simple input text for searching
in the grid.
My attempt (but ng-keyup require angularjs > 1.1.3 and I've got
1.0.7)
<input type="text" ng-keyup="mySearch()" ng-model="searchText">
$scope.getPagedDataAsync = function (pageSize, page, searchText) {
setTimeout(function () {
var data;
if (searchText) {
var ft = searchText.toLowerCase();
$http.get('largeLoad.json?q='+encodeURIComponent(ft)).success(function (largeLoad) {
$scope.setPagingData(largeLoad,page,pageSize);
});
} else {
$http.get('largeLoad.json').success(function (largeLoad) {
$scope.setPagingData(largeLoad,page,pageSize);
});
}
}, 100);
};
$scope.mySearch = function(){
console.log($scope.searchText);
$scope.getPagedDataAsync($scope.pagingOptions.pageSize, $scope.pagingOptions.currentPage,$scope.searchText);
}
Bye
NB its a fake request against a json file just to make the example.
Update: I'm using ng-grid-1.3.2
Basically to solve this problem I think you can use a solution similar to what I've done below where I'm just watching the property of the model for changes and firing a function to do the filtering on the data set at that point.
The HTML for the text input
<input type="text" placeholder="Type to filter" ng-model="gardenModel.externalFilterText"/>
The JavaScript that filters the data set (also included the part I had a watch on a service to update the data in the first place too or if the data is refreshed to reapply the filter).
//This function is called every time the data is updated from the service or the filter text changes
$scope.filterGardens = function(filterText) {
//Basically everything in this function is custom filtering specific
//to the data set I was looking at if you want something closer to the
//real implementation you'll probably have to dig through the source (I believe they separated the search filter code into it's own file in the original project)
//Creating a temporary array so changes don't cause a bunch of firing of watchers
var tempToShow = [];
//doing case insensitive search so lower case the filter text
filterText = filterText.toLowerCase();
//If the filter text is blank just use the whole data set
if(!filterText || filterText == "")
{
$scope.gardenModel.shownGardens = $scope.gardenModel.gardens;
return;
}
//step through each entry in the main list and add any gardens that match
for (var i = 0; i < $scope.gardenModel.gardens.length; i++) {
var curEntry = $scope.gardenModel.gardens[i];
var curGarden = curEntry.curGarden;
if(curGarden["Garden Name"] && curGarden["Garden Name"].answer.toString().toLowerCase().indexOf(filterText)!=-1)
tempToShow.push(curEntry);
else if(curGarden["Address"] && curGarden["Address"].answer.toString().toLowerCase().indexOf(filterText)!=-1)
tempToShow.push(curEntry);
else if(curGarden["Ownership"] && curGarden["Ownership"].answer.toString().toLowerCase().indexOf(filterText)!=-1)
tempToShow.push(curEntry);
else if(curGarden.gardenId && curGarden.gardenId == filterText)
tempToShow.push(curEntry);
};
$scope.gardenModel.shownGardens = tempToShow;
}
//Watch for any changes to the filter text (this is bound to the input in the HTML)
$scope.$watch('gardenModel.externalFilterText', function(value) {
$scope.filterGardens(value);
});
//Watch for any changes on the service (this way if addition/edit are made and
//refresh happens in the service things stay up to date in this view, and the filter stays)
$scope.$watch( function () { return gardenService.gardens; }, function ( gardens ) {
$scope.gardenModel.gardens = gardens;
$scope.filterGardens($scope.gardenModel.externalFilterText);
});
Edit Cleaned up the code formatting a bit and added some comments.
Previously I asked how to do this and was directed to this:
<script>
jQuery.fn.filterByText = function(textbox) {
return this.each(function() {
var select = this;
var options = [];
$(select).find('option').each(function() {
options.push({value: $(this).val(), text: $(this).text()});
});
$(select).data('options', options);
$(textbox).bind('change keyup', function() {
var options = $(select).empty().scrollTop(0).data('options');
var search = $.trim($(this).val());
var regex = new RegExp(search,"gi");
$.each(options, function(i) {
var option = options[i];
if(option.text.match(regex) !== null) {
$(select).append(
$('<option>').text(option.text).val(option.value)
);
}
});
});
});
};
</script>
(http://www.lessanvaezi.com/filter-select-list-options/)
When I use this filter on the select box it filters both the unselected AND the selected. I'd like it to ONLY filter the unselected because if a user wants to ammend the selections and filters again, the previously selected items go away - unless they meet the filter criteria.
I'm not that good at JavaScript or JQuery and can't understand how I might tell the above script to ignore options that are ":selected" but filter all else.
Here's a jfiddle if it helps: http://jsfiddle.net/UmKXy/ I'd like option one and two to remain selected and in the list when user begins to type.
Thanks for help!
The solution you had would not work with selected elements because he created an array of options at the start and then matched those options against the regex(Without regards to what is actually selected). I've used spans to hide options in the past and created an example for you to see how it works. Here is the link : http://jsfiddle.net/rD6wv/
Here is the code
$(function() {
$("#filterByText").bind('keyup',function(){
var search = $.trim($(this).val());
var regex = new RegExp(search,"gi");
$("#filez").find('option').each(function(){
if(!$(this).is(':selected')){
if($(this).val().match(regex) === null) {
$(this).wrap('<span>');
}else if($(this).parent().is('span')){
$(this).parent().replaceWith($(this));
}
}
});
});
});
You simply need to loop through all the options of the select when you type in the textbox.
You then check if it is selected, if it is you do nothing, else you check if it matches the search filter, if it does you wrap it in a span, making it invisible, else it means you need to see it, so you check if it is already wrapped in a span, and in that case you replace it with the option so you can see it again.
to selected the non selected options, use this:
$('option:not[selected]') or $('#myselect > option:not[selected]')
to remove them, use this:
$('option:not[selected]').remove();
in css, :not filters for opposite of what comes in the curved brackets.
and [] is attribute selector.
so :not[selected] means: does not have an attribute whose key is "selected"
I have a lot of elements with the same class. These elements are divided into groups by means of attribute "data-xxx"
<div class="myclass" data-n="group1"></div>
<div class="myclass" data-n="group1"></div>
<div class="myclass" data-n="group1"></div>
....
<div class="myclass" data-n="group2"></div>
<div class="myclass" data-n="group2"></div>
...
<div class="myclass" data-n="group3"></div>
...
...
How to perform a function on each item, but only once in each group using something like this?
$('.myclass').each(function(){
/// My function
});
Working example: http://jsfiddle.net/5sKqU/1/
$(document).ready(function() {
var group = {}; //an object that we're going to use as a hash
$('.myclass').each(function(){
if (group[$(this).attr('data-n')] != true) {
group[$(this).attr('data-n')] = true;
//do something here once per each group
alert('Group: '+ $(this).attr('data-n'));
}
});
});
I'm assuming that you only need this to run once on page load. If you could share more about your requirements, I can show you what changes you'll need to make.
Something like this maybe :
var groups = {};
$('.myclass').each(function(i, el) {
var n = $(el).data('n');
if(!groups[n]) {
groups[n] = $();
}
groups[n] = groups[n].add(el);
});
//At this point the object `groups` has one property per group,
//each value being a jquery collection comprising members of the group.
//Now the world's your oyster. You can loop through the groups
//with full access to each group's members if necessary.
$.each(groups, function(groupName, jq) {
//my function
});
You can set a certain HTML attribute on all group elements after processing the first one. After that, you can check the value of that attribute:
$('.myclass').each(function(){
if($(this).attr("processed")!="true") {
// process...
$("[data-n=" + $(this).attr("data-n")).attr("processed", "true");
} else {
// skip, or do something else with the element
}
});
You can select out each group with the jQuery attribute selector, like this:
$( '.myclass[data-n="groupX"]' );
I'm not sure if you meant that you only wanted to apply your function to one element in each group, but if so this will give you only the first in each:
$( '.myclass[data-n="groupX"]' ).first();
Given that you label your groups 1 through N you can just check if there is anything in the resulting set to determine when you have looped through every group.