Select2 Custom Matcher to keep options open if group title matches - javascript

I hope my question makes sense - wasn't sure on the best way to describe this. I have a grouped Select2 select form input something like this:
Vegetables
Lettuce
Tomatoes
Onions
Fruit
Apples
Oranges
Bananas
Spreads
Vegemite
Nutella
Peanut Butter
So you start typing App and of course you get Apples from the Select2 dropdown. If you type veg you get Vegemite and the Vegetables group heading but all the options are hidden. I would like to keep all the group options visible if a search term matches the group heading.
I did some digging in the select2 source code and I think it's actually easy but I could be wrong and if I am right I am stuck on how to make it work. Here is the source code:
https://github.com/select2/select2/blob/81a4a68b113e0d3e0fb1d0f8b1c33ae1b48ba04f/src/js/select2/defaults.js:
and a Gist I created vs. trying to paste it in here:
https://gist.github.com/jasper502/40b810e55b2195476342
I switched the order of the code and made some slight variable name changes to reflect this. I think this would keep the option group open. I tried to make a custom matcher based on this (see my Gist) but I was stuck at the point where it calls DIACRITICS:
https://github.com/select2/select2/blob/8ad8f200ba8c9429d636453b8ee3bcf593e8c87a/src/js/select2/diacritics.js
After some Googling I realized that this is replacing accented characters which I know I don't have so I removed that portion.
Now my matcher fails with TypeError: data.indexOf is not a function. (In 'data.indexOf(term)', 'data.indexOf' is undefined)
errors in my browser.
I am sure I am very close here but I am a bit over my head and beyond my experience and/or skill level to finish this off. Any pointers or ideas would be appreciated.
UPDATE
Here is a JSfiddle with what I am working with:
https://jsfiddle.net/jasper502/xfw4tmbx/9/

What I gather from your question is you want to be able to show options for selection when there's a match in either the option text OR the option's parent optgroup value attribute.
This is relatively straightforward: Mainly, look at both of the values and if either matches, return true using Select2's matcher option:
(Note: Using Select2 v3.5.4.)
(function() {
function matcher(term, text, opt) {
var $option = $(opt),
$optgroup = $option.parent('optgroup'),
label = $optgroup.attr('label');
term = term.toUpperCase();
text = text.toUpperCase();
if (text.indexOf(term) > -1
|| (label !== undefined
&& label.toUpperCase().indexOf(term) > -1)) {
return true;
}
return false;
}
$(".select2").select2({
matcher: matcher
});
})();
https://jsfiddle.net/xfw4tmbx/2/
v4.* and above changed the term and text to a more complex object, so it'll be slightly different, but the main concept is the same. As you can see, all I'm doing is using jQuery to select up to the option's parent if it's an optgroup element and including that in the matcher check.
Also, an optgroup will display if any of it's children are shown, so you only have to worry about displaying one or more of the option's, and not actually "show" the optgroup by manually showing it.
If you have a different requirement, please provide a (working/non-working?) demonstration fiddle showing what you have where we can actually run it.
EDIT
Select2 custom matching changed significantly with the 4.0 release. Here is a custom matcher that was posted to this GitHub issue. It is reproduced as-is below for completeness.
Notice that it's calling itself for child elements (the option elements within the optgroup elements), so modelMatcher() is running against both the optgroup and the option elements, but the combined set is returned after removing the optgroup and option elements that don't match. In the version above, you got every option element and simply returned true/false if you wanted it (and the parent) displayed. Not that much more complicated, but you do have to think about it a little bit more.
(function() {
function modelMatcher(params, data) {
data.parentText = data.parentText || "";
// Always return the object if there is nothing to compare
if ($.trim(params.term) === '') {
return data;
}
// Do a recursive check for options with children
if (data.children && data.children.length > 0) {
// Clone the data object if there are children
// This is required as we modify the object to remove any non-matches
var match = $.extend(true, {}, data);
// Check each child of the option
for (var c = data.children.length - 1; c >= 0; c--) {
var child = data.children[c];
child.parentText += data.parentText + " " + data.text;
var matches = modelMatcher(params, child);
// If there wasn't a match, remove the object in the array
if (matches == null) {
match.children.splice(c, 1);
}
}
// If any children matched, return the new object
if (match.children.length > 0) {
return match;
}
// If there were no matching children, check just the plain object
return modelMatcher(params, match);
}
// If the typed-in term matches the text of this term, or the text from any
// parent term, then it's a match.
var original = (data.parentText + ' ' + data.text).toUpperCase();
var term = params.term.toUpperCase();
// Check if the text contains the term
if (original.indexOf(term) > -1) {
return data;
}
// If it doesn't contain the term, don't return anything
return null;
}
$(".select2").select2({
matcher: modelMatcher
});
})();
https://jsfiddle.net/xfw4tmbx/16/

Related

Allow Only Selected Values From Dropdown List in Auto Complete Field

I need an autocomplete feature that allows a user to only type/choose from the list of values from a DB table. I do not want them to be able to enter free text after the reason is selected.
I have referred to a similar posting jQuery UI AutoComplete: Only allow selected valued from suggested list1 but I have a different issue.
The challenge is the list of acceptable options have special characters, specifically "(" and ")". When I type an option with ( I can see it in the drop down but when I use the arrow keys to select it will go to the previous option.
The array of acceptable options from PHP are accessed in javascript using
var validOptions = <?php echo json_encode($validOptionsArray); ?>;
The javascript is
previousValue = "";
$('.getValidOptions').autocomplete({
source: validOptions,
}).keyup(function() {
var isValid = false;
for (i in validOptions) {
if (validOptions[i].toLowerCase().match(this.value.toLowerCase())) {
isValid = true;
}
}
if (!isValid) {
this.value = previousValue;
} else {
previousValue = this.value;
}
});
I understand how this is happening. If I remove the line
this.value = previousValue;
it will allow me to select the option with ( but then the user can continue typing.
However, I want to keep this line as it prevents the user from adding text to the dropdown.
I can use the mouse to select the option but I get an error in the console
Uncaught SyntaxError: Invalid regular expression: /(/
So it seems the issue is related to keyup function
I have tried (1) charset="utf-8" and (2) regular expression such as
(this.value).replace(/([.*+?^=!${}()|[\]\/\\])/g, '\\$1');
Any thoughts on how to fix this?
Thanks.
The data in the json_encode($validOptionsArray) is
Array
(
[0] => NI: New Member
[1] => NI: New Member (referred from current member)
[2] => RI: Returning Member
[3] => RI: Returning Member (with conditions)
[4] => XI: Exclusive member (full/all access)
)
I decided to go with jQuery inputToken http://loopj.com/jquery-tokeninput/
It is easy to implement and allows the user to only select the available options and not edit them. Maybe others will find this useful.

JavaScript - if class list contains value from Array

I'm trying to setup a filter on a database application, for lost property for a scout camp. The idea is the following:
The lost property items are logged into the database by reception as either 'Lost', 'Handed In', 'Owner Notified' or 'Returned' (meaning 4 lists generated from a table, with a status flag which is used to filter which list an item appears in).
At the top of each list, I have a form field (text) which I would like to use to filter the list, simply by the reception team member typing in some words that describe the item (eg, Jacket blue)
I've set a common class name to each row in the list table (a different one for each list, so listLost, listFound, listNotified, listReturned)
I've then used some of the database fields to add additional classes to the list (eg, item, colour, first name, surname - these are converted to UCASE to get around the case sensitive nature of JavaScript), so the final class might look like:
class="listLost JACKET BLUE FRED BLOGGS"
The reception team member can then type into the text field, something like 'Jacket Blue' and my JavaScript function is supposed to filter the list as follows:
the input from the text field is split into an array, using SPACE as the separator
it looks for all items in the page with a particular common class name (in this example, it would be listLost)
it then pages through the array comparing the class list for each (in this case, table row) with the array value and it if finds a match, the table row will be displayed, if not, it won't
Here's my JavaScript function:
function filterLostProperty(filterField,filterList)
{
var filterStr = document.getElementById(filterField).value;
var filterVals = filterStr.split(" ");
var filterItems = document.getElementsByClassName(filterList);
var displayCheck = 0
for (x = 0; x < filterItems.length; x++)
{
for (y = 0; y < filterVals.length ; y++)
{
if (filterItems[x].classList.contains(filterVals[y].toUpperCase()))
{
displayCheck++
}
}
if (displayCheck > 0)
{
filterItems[x].style.display = "table-row";
}
else
{
filterItems[x].style.display = "none";
}
}
}
The form field has:
onChange="filterLostProperty('filterLost','listLost')
where filterLost is the ID of the text field from which the search string comes.
PROBLEM: it doesn't really filter in the way it should... some strings just generate everything, some bring up the item you wanted plus 2 you didn't, some don't generate anything at all. And when there is more than one word it goes even more weird.
Does anyone have any suggestions about where I might have gone wrong here? Or is my method here just too overly complicated and I'm missing a simple trick with something?
21.11.2017
So - I edited my function further after having a bit of an idea that I would instead of generating an Array of the filter text (as the array approach was only using the last entered word as the filter text), I would just create a string with AND between each one (using a replace function to replace SPACE with AND Operator), in the hope that it would do a 'If class list contains X and Y and Z then blah blah'...
I tested this with fixed values at first on my test page:
function manualFilter(filterList)
{
var filterItems = document.getElementsByClassName(filterList);
for (x = 0; x < filterItems.length; x++)
{
{
if (filterItems[x].classList.contains("WALLET" && "BLUE"))
{
filterItems[x].style.display = "table-row";
}
else
{
filterItems[x].style.display = "none";
}
}
}
}
My test page is at https://bookings.springbank.org.uk/testFilter.asp and the test filter can be triggered by clicking the 'Manual Filter' button.
Problem is that when I do this, I get the row with WALLET BLUE in as expected (and not the row with WALLET BLACK which is good)... but I also get the row with JACKET BLUE (presumably because it's matched BLUE)... ideally, I don't want this to display.
If this can be made to work (effectively replacing the spaces from the text field entry with an AND operator to be interpreted by the IF statement)... the only other bit I'm not totally clear on (not being particularly proficient in JavaScript) is how I can do this dynamically from the function... presumably it's going to be a case of a certain number of consecutive quotes so that the && is interpreted as and AND operator and not simply as part of a string?

Understanding and correcting the structure of Javascript code

so I've been on here for awhile, and I'm still considered an entry level programmer based on my general knowledge of structure and basic concepts. I have a function below that was given to me in an answer for a different question I asked. I can understand most of what it is doing, but I need help understanding the rest of what it does. I'm asking this because I would really like to understand further advanced concepts of javascript, and jQuery.
So what I've done below is placed the function, and I'll comment in what I know about what the function is doing at where, and then I'll place question marks where I"m confused.
function validate(){
//array of objeccts used to defined the class selector for each element iterated
//with what validation function is be assigned to that specific selector
var fields = [
{
selector: $('.not-empty'),
validations: [ isNotEmpty]
},
{
selector: $('.email'),
validations: [ isNotEmpty, isEmail]
},
{
selector: $('.number'),
validations: [ isNotEmpty, isNumber]
},
{
selector: $('.number-noreq'),
validations: [isNumberNotRequired]
},
{
selector: $('.checked'),
validations: [isChecked]
}
];
//remove any classes of 'has-error' from each element traversed before validation begins
$('.form-control').closest('.form-group').removeClass('has-error');
//defining variables
var i = 0, k = 0, z = 0, j = fields.length, item, selector, fn, info;
//for loop to traverse the fields array of objects
for(; i < j; i++){
item = fields[i];
//traversing each field.validation
for(k = 0; k < item.validations.length; k++){
fn = item.validations[k]; //setting fn as a function found in validation
//traversing each selector in item
for( z = 0; z < item.selector.length; z++){
selector = $(item.selector[z]); //setting the selector
//attempting to set info to the closest form or input group found by the selector
info = selector.closest('.form-group, .input-group');
if(info) //if info contains data
//?????????????????????????????????????? no idea what's going on below other
//other than it's running the validation function that was passed, but why
//is it written like this and what is it doing?
info[fn(selector.val()) ? 'removeClass' : 'addClass']('has-error');
}
}
}
}
So that is the basic question I have for this code (where all the question marks are). If someone can clearly answer what is going on, why you write the code like that, what the purpose of it is, and is it benefcial or not, would be fantastic. if you need more clarification I would be happy to provide it. I just want to be able to explain the code to somebody and know what I am talking about instead of trying to have to bs my through it. I think it was Einstein who said, "If you can't explain something accurately and to the point, then you truly do not understand it" or something like that!
Thank you in advance!
EDIT: here are the functions that 'validations' traverse through
//validation functions
function isNotEmpty(value){
return value && $.trim(value).length > 0;
}
function isEmail(value){
return /^([^#\s\t\n]+\#[\w\d]+\.[\w]{2,3}(\.[\w]{2})?)$/.test(value);
}
function isNumber(value){
return /^\d+$/.test(value);
}
function isNumberNotRequired(value){
return /^\d+$/.test(value) || value.length < 1;
}
function isChecked(value){
var r = false;
var name = $(value).attr('name');
$('input[name="'+name+'"').each(function(){
if($(this).is(':checked')){
r = true;
}
});
return r;
}
SECOND EDIT/UPDATE: We have determined that there is a severe error in the code that allows it not to keep track of the validation and take into account previous validations for input groups, and other related sections. How does this corrected. I'm testing items on jsfiddle at the moment I will return when I have restuls!
This line:
info[fn(selector.val()) ? 'removeClass' : 'addClass']('has-error');
is equivalent to this:
var result = fn(selector.val());
if (result)
info.removeClass("has-error");
else
info.addClass("has-error");
How is that? Well, your code calls the function plucked from the list of validation routines stored in that data structure, passing the value of the field to be tested. The result of that function call is used as a true/false test in the ? : expression. If the result is true, the ? : resolves to the string "removeClass"; if false, to "addClass".
Now, what is info? It's a jQuery object that refers to the closest piece of the DOM that (presumably) is where an error message would be displayed, or where some other indicator would be shown based on some CSS rule. The [ ] operator will take whichever of those two strings the ? : resolves to and use that as a property accessor. The net effect, therefore, is to reference either info.removeClass or info.addClass. Those are both references to jQuery methods, so one or the other will be called. In either case, the code wants to operate on the class name "has-error", because it wants to either add it (when the validation fails) or remove it (when the validation succeeds).
That said, the code has a serious defect: if, for a given field, there is in fact a list of validation functions, the code will run all of them (which is fine). However, for each validation function, it sets or clears that "has-error" class without regard to prior validation results. That might work, if you're really careful with the ordering of the validation functions, but that's an awfully fragile way of doing things. I think it would be much more robust if it made each test and kept track of whether any test failed, and then after that process is complete for a given field it'd only then set or clear the "has-error" class.
Fixing the code isn't too hard. Currently it iterates the the validation functions outside the iteration over the selected fields, which (I think) is backwards. However, as long as it checks the state of the error indicator element(s), it should be OK.
First, at the top, the code removes "has-error" from .form-group elements but not from .input-group elements. That's clearly incorrect, so:
$('.form-control').closest('.form-group, .input-group').removeClass('has-error');
Then, in the loop:
for( z = 0; z < item.selector.length; z++){
selector = $(item.selector[z]); //setting the selector
//attempting to set info to the closest form or input group found by the selector
info = selector.closest('.form-group, .input-group');
if (info.length && !fn(selector.val())) // if info contains data and field is invalid
info.addClass('has-error');
}
Since all the "has-error" flags are cleared at the outset, all we need to do is add the class to classes that are invalid. If you wanted to have a positive "is-ok" class, then you'd add that to everything at the top and remove it when you find an error.
As you should have known, foo.bar are foo["bar"] are identical in JavaScript (if you did not know, learn it, now).
This line
info[fn(selector.val()) ? 'removeClass' : 'addClass']('has-error');
means
var methodName;
if (fn(selector.val())) { methodName = 'removeClass'; } else { methodName = 'addClass'; }
info[methodName]('has-error')
so, in yet another words,
if (fn(selector.val())) {
info.removeClass('has-error');
} else {
info.addClass('has-error');
}
So it is actually switching class has-error on/off. Just it's pretty densely written.

jQuery UI autocomplete - matched option not getting selected on focusOut

I have made a jQuery UI - autocomplete in which I have added the following conditions:
Sorted option list
mandatory select an option
Here I made a mistake that, when I type the exact option, it is not getting selected.
For example:
var json = ["JOAQUIN", "BERNARDINO", "MODOC", "ALASKA", "MADERA", "ANDERSON",
"Kingston", "Kala", "Aka"];
I type aka, it is not accepting.
I also tried Aka, still it is not accepting.
How to reproduce:
After typing the above text in textbox, just focusout - The value
will be emptied. I want to handle aka = Aka = AKA (with/without selecting from option list)
PS: I'm not asking for code, I have made a small blunder which I need it to be correct.
Here is my fiddle, where you can easily reproduce the exception.
How can I change my code to work like this. Please share your suggestions and point me in a right direction on placing the conditions in order.
What if you change the event to close instead of the change event. Then check if the value is in the array and if not clear the container. This will require you match capitalization when you enter the value. If you wanted to ignore capitalization you could do a convert the json array to lower case and the value to lowercase before search the array.
fiddle
var json = ["JOAQUIN", "BERNARDINO", "MODOC", "ALASKA", "MADERA", "ANDERSON", "Kingston", "Kala", "Aka"];
$('input').autocomplete({
source: function (request, response) {
var matches = $.map(json, function (json) {
if (json.toUpperCase().indexOf(request.term.toUpperCase()) === 0) {
return json;
}
});
response(matches);
},
close: function(event, ui) {
var index = jQuery.inArray($('input').val(), json);
if (index == -1) {
$('input').val("");
}
}
});
As far as I can see from the documentation the UI property only gets set when a dropdown item is selected (clicked on) so you will have to add to your validation in the change function to manually check against the values.

CodeMirror - Using RegEx with overlay

I can't seem to find an example of anyone using RegEx matches to create an overlay in CodeMirror. The Moustaches example matching one thing at a time seems simple enough, but in the API, it says that the RegEx match returns the array of matches and I can't figure out what to do with it in the context of the structure in the moustaches example.
I have a regular expression which finds all the elements I need to highlight: I've tested it and it works.
Should I be loading up the array outside of the token function and then matching each one? Or is there a way to work with the array?
The other issue is that I want to apply different styling depending on the (biz|cms) option in the regex - one for 'biz' and another for 'cms'. There will be others but I'm trying to keep it simple.
This is as far as I have got. The comments show my confusion.
CodeMirror.defineMode("tbs", function(config, parserConfig) {
var tbsOverlay = {
token: function(stream, state) {
tbsArray = match("^<(biz|cms).([a-zA-Z0-9.]*)(\s)?(\/)?>");
if (tbsArray != null) {
for (i = 0; i < tbsArray.length; i++) {
var result = tbsArray[i];
//Do I need to stream.match each element now to get hold of each bit of text?
//Or is there some way to identify and tag all the matches?
}
}
//Obviously this bit won't work either now - even with regex
while (stream.next() != null && !stream.match("<biz.", false)) {}
return null;
}
};
return CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || "text/html"), tbsOverlay);
});
It returns the array as produced by RegExp.exec or String.prototype.match (see for example https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/match), so you probably don't want to iterate through it, but rather pick out specific elements the correspond to groups in your regexp (if (result[1] == "biz") ...)
Look at implementation of Code Mirror method match() and you'll see, that it processes method parameter for two types: string and RegExp.
Your constant in
stream.match("<biz.")
is of string type.
Define it in RegExp type:
tbsArray = /<biz./g
Thus, your stream will be matched with RegExp.

Categories

Resources