JQuery Autocomplete Widget - Up & Down Arrow Keys not working - javascript

I am new to Jquery and was going through a snippet of code of a larger application (which we have outsourced). This snippet is a jquery widget to autocomplete results.
On the HTML page, we have a search box. When the user enters some keystrokes, a search is performed (for places and events) on our database and the results are shown in an un-ordered list. The results show up fine. However, I was hoping to scroll through the results using the down and up arrow key which does not seem to work.
Opening the Developer Tools in Chrome, when I press the Down key the following error is reported
Uncaught TypeError: Cannot read property 'value' of undefined jquery-ui.min.js:8
The relevant Widget code is as follows
$.widget("custom.catcomplete", $.ui.autocomplete, {
_renderMenu: function(ul, items) {
ul.addClass('search-box');
var that = this;
var i = 0;
var j = 0;
var index = 0;
$.each(items, function(index, item) {
if (item.Space && i == 0) {
ul.append('<h4><i class="fa fa-car fa-fw"></i> Parking Space</h4>');
i++;
}
if (item.Event && j == 0) {
if (i > 0) {
ul.append('See All Result');
}
ul.append('<h4><i class="fa fa-calendar"></i> Venues</h4>');
j++
}
that._renderItemData(ul, item);
});
if (j == 0 && items.value == 'error') {
ul.append('See All Result');
}
}
});
$(function() {
// made on 01-07-2015
$("#search_new").catcomplete({
delay: 0,
//minLength: 2,
zIndex: 9999,
source: base_url + "searches/index.json",
messages: {
noResults: '',
results: function() {}
},
select: function(event, ui) {
window.location = (ui.item.url);
}
}).off('blur').on('blur', function() {
if(document.hasFocus()) {
$('ul.ui-autocomplete').show();
}
});
});
$.ui.autocomplete.prototype._renderItem = function(ul, item) {
if (item.Space) {
var space_body = '';
var address2 = (item.Space.address2 != null) ? item.Space.address2 : '';
space_body = space_body + "<article><strong><a class='search-name-link' href="+base_url+"spaces/spaceDetail/"+encodeURIComponent(Base64.encode(item.Space.id))+">"+item.Space.name+"</a> - "+item.City.name+"</strong></br>"+item.Space.flat_apartment_number+", "+item.Space.address1+", "+address2+" "+item.City.name+"</article>";
console.log("In _renderItem");
return $(ul).append(space_body)
.appendTo(ul);
}
if (item.Event) {
var event_body = '';
event_body = event_body + "<article><strong><a class='search-name-link' href="+base_url+"events/eventDetail/"+encodeURIComponent(Base64.encode(item.Event.id))+">"+item.Event.event_name+"</a></strong></br>"+item.EventAddress.address+" "+item.City.name+"</article>";
return $(ul).append(event_body)
.appendTo(ul);
}
if (!item.Space && !item.Event) {
return $(ul).append("<article class='no-search'>No records found. <b><a href='javascript:void(0)' class='search-not-found'>Click here</a></b> to get notification for your keyword.</article>")
.appendTo(ul);
}
}
From my limited understanding of JQuery Widgets :
For each result received we are appending to a ul and calling renderItemData which in turn calls the renderItem
I am unable to understand which 'value' is the code referring to.

Related

removing eventhandler seems not to work

I have the next code (code does not run like copied here) that I would like to use for addressing errors on an input field of a form. The field is checked when the focus of the field is lost. An ajax request checks if the entry is allowed for that field (based on a lookup table). The two user functions set and remove error class around the input field.
When an error occurs, I want the field in error to have the only focus of the whole form (user has to correct this field before a different field is selected).
That last requirement does not work in the code. The focus is handed intermitted to the fields. It seems like if I have to click more then needed as well.
Question: What am I doing wrong?
$(document).ready(function() {
var errors = [];
//check value in lookup table with ajax
$("body").on("blur", "input[data-table]", function() {
var name = $(this).attr("id");
var table = $("input[name=" + name + "]").data().table;
var value = $(this).val();
var len = value.length;
//Only if an entry has been made in the field
if (len > 0) {
var jqxhr = $.post("/controller/lookup.php", {
table: table,
value: value
}, function() {});
//Done with the request
jqxhr.done(function(data) {
var e = JSON.parse(data).error;
if (e == "true") {
//set error
setError(name);
//prevent focus on other
$("body").on("click", "input", function(e) {
if (!(e.target.id == name))
$("input#" + name).focus();
});
} else {
removeError(name);
$("body").off("click", "input");
}
});
//A failure has occured
jqxhr.fails(function(xhr, msg) {
//TODO: exception handling
});
}
//if the field has been cleared remove error
removeError(name);
});
function setError(name) {
//Check if error already exists
if (!getErrors(name)) {
var e = getErrorIndex(name);
if (e > -1) errors[e].error = true;
else errors.push({
name: name,
error: true
});
var span = "<span class=\"glyphicon glyphicon-exclamation-sign\" style=\"color: red; float: left\"></span>";
//Decorate errors
$("input#" + name).parent().addClass("has-error");
$("input#" + name).after(span);
};
}
function removeError(name) {
var e = getErrorIndex(name);
if (e > -1) {
errors[e].error = false;
$("input#" + name).parent().removeClass("has-error");
$("input#" + name).next().remove();
}
}
function getErrors(needle) {
for (var i = 0; i < errors.length; i++) {
if (errors[i].name === needle) return errors[i].error;
}
return false;
}
function getErrorIndex(needle) {
for (var i = 0; i < errors.length; i++) {
if (errors[i].name === needle) return i;
}
return -1;
}
});
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>

Store and retrieve search results

I'm a bit stuck with a piece of code. I have a search field which calls a PHP function which looks into the database:
$this->_helper->layout->disableLayout();
$q = $this->getRequest()->getParam('q');
$product_db = new Products();
$this->view->products = $product_db->searchProduct($q);
foreach($this->view->products as $product)
{
....
}
The result of this gets loaded into my HTML page via JQuery:
var searchItem = function() {
if($('#json_search').val())
{
$('#search-resultlist').load('/search/quick/q/'+ $('#json_search').val() );
}
else {
$('#search-resultlist').html('');
}
}
HTML:
<div class="right-searchcontent" >
<input type="text" class="form-control" placeholder="Search ... " id="json_search">
</div>
<div class="right-searchresult">
<ul id="search-resultlist" >
<li >
</li>
</ul>
</div>
Now basically what I try to achieve is to create a 'search history'.
At first I tried it with a SESSION array in my search controller:
if(!isset($_SESSION['history'])) {
$_SESSION['history'] = array();
}
And in my function to show the database search results:
if(!empty($product)){
if(!in_array($product->naam, $_SESSION['history']))
{
$_SESSION['history'][] = $product->naam;
}
}
But this was storing ALL the values I ever searched for (like: 'sk', 'ah')
I just want the values I actually clicked on.
Can anyone point me in the right direction?
I've been trying to achieve my result with localStorage, but this wasn't going to give me the right solution. Also I tried using Cookies with this function:
var cookieList = function(cookieName) {
var cookie = $.cookie(cookieName);
var items = cookie ? cookie.split(/,/) : new Array();
return {
"add": function(val) {
//Add to the items.
items.push(val);
//Save the items to a cookie.
$.cookie(cookieName, items.join(','));
},
"remove": function (val) {
indx = items.indexOf(val);
if(indx!=-1) items.splice(indx, 1);
$.cookie(cookieName, items.join(',')); },
"clear": function() {
items = null;
//clear the cookie.
$.cookie(cookieName, null);
},
"items": function() {
return items;
}
}
}
But when I tried to alert list.items it just returned me the whole method.
I can achieve the product name when I click it, but I just don't know how I can store this into a SESSION or something else what I can achieve any time on any page..
$(document).on('click', 'a.searchItem', function(e){
e.preventDefault();
var search = $(this).attr('value'));
});
I'ts been fixed with the function I already had:
var cookieList = function(cookieName) {
var cookie = $.cookie(cookieName);
var items = cookie ? cookie.split(/,/) : new Array();
return {
"add": function(val) {
//Add to the items.
items.push(val);
//Save the items to a cookie.
$.cookie(cookieName, items.join(','));
},
"contain": function (val) {
//Check if an item is there.
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function(obj, start) {
for (var i = (start || 0), j = this.length; i < j; i++) {
if (this[i] === obj) { return i; }
}
return -1;
};
}
var indx = items.join(',').indexOf(val);
if(indx > -1){
return true;
}else{
return false;
} },
"remove": function (val) {
indx = items.indexOf(val);
if(indx!=-1) items.splice(indx, 1);
$.cookie(cookieName, items.join(',')); },
"clear": function() {
items = null;
//clear the cookie.
$.cookie(cookieName, null);
},
"items": function() {
return items;
}
}
}
When someone clicks on a search result it's added to the cookie list:
$(document).on('click', 'a.searchItem', function(e){
var search = $(this).attr('value');
var list = new cookieList("MyItems");
if(!list.contain(search)){
list.add(search);
}
});
And when opening the search box I show the results of the list:
jQuery.each(list.items(), function(index, value){
....
});
Maybe this will help someone out sometime..

knockout js drop down list not showing my binding within a foreach

I am not getting any errors, but for some reason (likely simple) I can't get values to show up in my drop down list.
My knockout vm:
function UserRecruitingViewModel(apiBaseUrl, userId) {
var self = this;
self.orgs = ko.observableArray();
//ddl org view switcher stuff
self.orgDdl = ko.observableArray();
self.selectedOrg = ko.observable();
var DropdownOrg = function (name, guid) {
this.orgName = name;
this.orgId = guid;
};
//pull initial data
$.getJSON(apiBaseUrl + "?userId=" + userId, function (data) {
//alert(data);
$.each(data, function () {
//alert(data);
var dropdownOrg = new DropdownOrg();
var org = new Organization();
$.each(this, function (k, v) {
//alert("in foreach");
if (k == "UserGuid") {
org.userGuid = v;
}
if (k == "OrgGuid") {
dropdownOrg.orgId = v;
org.orgGuid = v;
}
if (k == "OrgName") {
dropdownOrg.name = v;
org.orgName = v;
}
if (k == "IsHiring") {
org.isHiring = v;
}
if (k == "Blurb") {
org.blurb = v;
}
//alert("k var: "+k); //field name
//alert("v var "+v); //field value
//alert("data var " + data); //array of objects (3)
//alert(org);
});
alert("ddl " + dropdownOrg.name);
self.orgDdl.push(dropdownOrg);
self.orgs.push(org);
});
});
}
in the alert("ddl " + dropdownOrg.name) I am seeing the correct values.
On my page I have this:
<ul class="list-unstyled" data-bind="foreach: orgs">
<div class="row">
<div class="col-md-6">
<p>
Viewing:
<select class="form-control"
data-bind="options: $parent.orgDdl(), optionsText: orgName, value: orgGuid, optionsCaption: 'Change...'"></select>
</p>
...snip...
When I pull up the page in a browser I get the alerts showing the proper data, but in the ddl I see 4 options: "Change..." and 3 blank options. Inspecting the elements showing the values are not in there.
Any idea where I am messing up the syntax?
Figured it out. I am not sure why this was they case, but I had to change the following:
In the VM:
var DropdownOrg = function (name, orgId) {
this.name = name;
this.orgId = orgId;
};
On the page:
<select class="form-control"
data-bind="options: $root.orgDdl, optionsText: 'name'"></select>
In the get request (in the VM):
//pull initial data
$.getJSON(apiBaseUrl + "?userId=" + userId, function (data) {
$.each(data, function () {
var dropdownOrg = new DropdownOrg();
var org = new Organization();
$.each(this, function (k, v) {
if (k == "UserGuid") {
org.userGuid = v;
}
if (k == "OrgId") {
dropdownOrg.orgId = v;
org.orgId = v;
}
if (k == "OrgName") {
dropdownOrg.name = v;
org.orgName = v;
}
if (k == "IsHiring") {
org.isHiring = v;
}
if (k == "Blurb") {
org.blurb = v;
}
});
self.orgDdl.push(dropdownOrg);
self.orgs.push(org);
});
});
After throwing in a bunch of alerts and strigifying a bunch of JSON I saw that I had some names in my json objects that did not match. I thought I had to follow the naming I set in the DropDownOrg object (I did change it to match, but see the original question for the old one), but it turns out it was basing the name off of how I was assigning it in the foreach loop inside the ajax get request. Seems odd to me, so if anyone wants to chime in on why this happens, feel free!

slickgrid collapsing tutorial

I am currently working with example 5: http://mleibman.github.io/SlickGrid/examples/example5-collapsing.html so that I can implement collapsing in my own slickgrid. However I am having trouble doing this and was wondering if there is some tutorial I can look at or if someone has done this and can give me a few pointers. The following is some test code that I have been working with to get this to work, without a lot of luck
<script>
var grid;
var data = [];
//this does the indenting, and adds the expanding and collapsing bit - has to go before creating colums for the grid
var TaskNameFormatter = function (row, cell, value, columnDef, dataContext) {
value = value.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
var spacer = "<span style='display:inline-block;height:1px;width:" + (15 * dataContext["indent"]) + "px'></span>";
var idx = dataView.getIdxById(dataContext.id);
if (data[idx + 1] && data[idx + 1].indent > data[idx].indent) {
if (dataContext._collapsed) {
return spacer + " <span class='toggle expand'></span> " + value;
} else {
return spacer + " <span class='toggle collapse'></span> " + value;
}
} else {
return spacer + " <span class='toggle'></span> " + value;
}
};
var columns = [
{id: "title", name: "title", field: "title", width: 150, formatter: TaskNameFormatter},
{id: "duration", name: "Duration", field: "duration"},
{id: "start", name: "Start", field: "start"},
{id: "finish", name: "Finish", field: "finish"}
];
var options = {
enableColumnReorder: false
};
var searchString = "";
function myFilter(item) {
if (searchString != "" && item["title"].indexOf(searchString) == -1) {
return false;
}
if (item.parent != null) {
var parent = data[item.parent];
while (parent) {
if (parent._collapsed || (searchString != "" && parent["title"].indexOf(searchString) == -1)) {
return false;
}
parent = data[parent.parent];
}
}
return true;
}
$(function () {
var indent = 0;
var parents = [];
for (var i = 0; i < 3; i++) {
var d = (data[i] = {});
var parent;
d["id"] = "id_" + i;
if (i===0){
d["title"] = "1st Schedule";
}else if(i===1){
d["title"] = "1st Schedule Late";
}else {
d["title"] = "1st Schedule Early Leave";
}
if (i===0){
parent =null;
}
if (i===1){
parent = parents[parents.length - 1];
indent++;
}
if (i===2){
indent++;
parents.push(1);
}
if (parents.length > 0) {
parent = parents[parents.length - 1];
} else {
parent = null;
}
d["indent"] = indent;
d["parent"] = parent;
d["duration"] = "5 days";
d["start"] = "01/01/2013";
d["finish"] = "01/01/2013";
}
/* **************Adding DataView for testing ******************/
dataView = new Slick.Data.DataView();
dataView.setItems(data);
dataView.setFilter(myFilter); //filter is needed to collapse
/* ************** DataView code end ************************* */
grid = new Slick.Grid("#myGrid", dataView, columns, options);
//this toggles the collapse and expand buttons
grid.onClick.subscribe(function (e, args) {
if ($(e.target).hasClass("toggle")) {
var item = dataView.getItem(args.row);
if (item) {
if (!item._collapsed) {
item._collapsed = true;
} else {
item._collapsed = false;
}
dataView.updateItem(item.id, item);
}
e.stopImmediatePropagation();
}
});
//this is needed for collapsing to avoid some bugs of similar names
dataView.onRowsChanged.subscribe(function (e, args) {
grid.invalidateRows(args.rows);
grid.render();
});
})
</script>
I strongly suggest you to look into the grouping example instead, it's also with collapsing but is made for grouping, which is 90% of what people want and since couple months can also do multi-column grouping. You can see here the SlickGrid Example Grouping then click on these buttons (1)50k rows (2)group by duration then effort-driven (3)collapse all groups...then open first group and you'll see :)
As for your code sample, I replaced the example with your code and it works just as the example is...so?

getting jQuery ui autocomplete to work with #mentions only

I've had a look at a few questions, such as https://stackoverflow.com/a/7222592/2332251
I'm still having trouble reconciling it with the code I have.
At the moment the following works perfectly for searching a username as I start typing.
$(function() {
$("#appendedInputButton").autocomplete({
minLength: 2,
source: "searchusers.php"
});
});
The function in searchusers.php outputs the usernames from the database.
As I said, I'm having trouble making other #mention solutions work for me. I've tried copying over other solutions and swapping my details in but nothing seems to work.
So...
What do I need to do to my current autocomplete script to make it load only
when I initially type the '#' symbol?
I would really like to be able to have multiple #mentions in my posts
(optional) when autocomplete suggests usernames and when I select the username from the list I want it to appear in my post with the #symbol still appended to the front of the username e.g. "hello #john, the # symbol is still attached to your username"
If you need more info, please comment and I will provide more :)
Edit I'm just really unsure of the syntax to make it work. For example, using the example answer I posted above, I came up with (but it doesn't work):
function split(val) {
return val.split(/#\s*/);
}
function extractLast(term) {
return split(term).pop();
}
function getTags(term, callback) {
$.ajax({
url: "searchusers.php",
data: {
filter: term,
pagesize: 5
},
type: "POST",
success: callback,
jsonp: "jsonp",
dataType: "jsonp"
});
}
$(document).ready(function() {
$("#appendedInputButton")
// don't navigate away from the field on tab when selecting an item
.bind("keydown", function(event) {
if (event.keyCode === $.ui.keyCode.TAB && $(this).data("autocomplete").menu.active) {
event.preventDefault();
}
}).autocomplete({
source: function(request, response) {
if (request.term.indexOf("#") >= 0) {
$("#loading").show();
getTags(extractLast(request.term), function(data) {
response($.map(data.tags, function(el) {
return {
value: el.name,
count: el.count
}
}));
$("#loading").hide();
});
}
},
focus: function() {
// prevent value inserted on focus
return false;
},
select: function(event, ui) {
var terms = split(this.value);
// remove the current input
terms.pop();
// add the selected item
terms.push(ui.item.value);
// add placeholder to get the comma-and-space at the end
terms.push("");
this.value = terms.join("");
return false;
}
}).data("autocomplete")._renderItem = function(ul, item) {
return $("<li>")
.data("item.autocomplete", item)
.append("<a>" + item.label + " <span class='count'>(" + item.count + ")</span></a>")
.appendTo(ul);
};
});
Where do I insert searchusers.php, #appendedInputButton and other specific info? I hope this makes sense.
I will form an answer based on my comments.
First of all lets review the list of requirements:
autocomplete usernames started with # symbol
prepend usernames with # symbol
multiple #mentions in a text
edit any #mention anywhere in a text
to implement the last requirement we need some magic functions that i found on stackoverflow:
getCaretPosition - https://stackoverflow.com/a/2897229/2335291
setCaretPosition - https://stackoverflow.com/a/512542/2335291
Also to detect a username somewhere in the text we need to define some constraints for usernames. I assume that it can have only letters and numbers and test it with \w+ pattern.
The live demo you can find here http://jsfiddle.net/AU92X/6/ It always returns 2 rows without filtering just to demonstrate the behavior. In the listing below i've put the original getTags function from the question as it looks fine for me. Although i have no idea how searchusers.php works.
function getCaretPosition (elem) {
// Initialize
var iCaretPos = 0;
// IE Support
if (document.selection) {
// Set focus on the element
elem.focus ();
// To get cursor position, get empty selection range
var oSel = document.selection.createRange ();
// Move selection start to 0 position
oSel.moveStart ('character', -elem.value.length);
// The caret position is selection length
iCaretPos = oSel.text.length;
}
// Firefox support
else if (elem.selectionStart || elem.selectionStart == '0')
iCaretPos = elem.selectionStart;
// Return results
return (iCaretPos);
}
function setCaretPosition(elem, caretPos) {
if(elem != null) {
if(elem.createTextRange) {
var range = elem.createTextRange();
range.move('character', caretPos);
range.select();
}
else {
if(elem.selectionStart) {
elem.focus();
elem.setSelectionRange(caretPos, caretPos);
}
else
elem.focus();
}
}
}
function getTags(term, callback) {
$.ajax({
url: "searchusers.php",
data: {
filter: term,
pagesize: 5
},
type: "POST",
success: callback,
jsonp: "jsonp",
dataType: "jsonp"
});
}
$(document).ready(function() {
$("#appendedInputButton").autocomplete({
source: function(request, response) {
var term = request.term;
var pos = getCaretPosition(this.element.get(0));
var substr = term.substring(0, pos);
var lastIndex = substr.lastIndexOf('#');
if (lastIndex >= 0){
var username = substr.substr(lastIndex + 1);
if (username.length && (/^\w+$/g).test(username)){
getTags(username, function(data) {
response($.map(data.tags, function(el) {
return {
value: el.name,
count: el.count
}
}));
});
return;
}
}
response({});
},
focus: function() {
// prevent value inserted on focus
return false;
},
select: function(event, ui) {
var pos = getCaretPosition(this);
var substr = this.value.substring(0, pos);
var lastIndex = substr.lastIndexOf('#');
if (lastIndex >= 0){
var prependStr = this.value.substring(0, lastIndex);
this.value = prependStr + '#' + ui.item.value + this.value.substr(pos);
setCaretPosition(this, prependStr.length + ui.item.value.length + 1);
}
return false;
}
}).data("ui-autocomplete")._renderItem = function(ul, item) {
return $("<li>")
.data("ui-autocomplete-item", item)
.append("<a>" + item.label + " <span class='count'>(" + item.count + ")</span></a>")
.appendTo(ul);
};
});
I cannot add a comment, so I'm just going to add this as an answer.
I tried the code snippet you've provided and it worked great. The only problem I had was while editing the mention. I decided to edit from the middle of the mention, the autocomplete showed and I selected an item successfully. Only - it didn't delete the rest of the previous mention, only the letters before the cursor's position.
So I added something extra:
select: function(event, ui) {
var pos = comments.init.getCaretPosition(this);
var substr = this.value.substring(0, pos);
var lastIndex = substr.lastIndexOf('#');
var afterPosString = this.value.substring(pos, this.value.length);
var leftovers = afterPosString.indexOf(' ');
if (leftovers == -1)
leftovers = afterPosString.length;
if (lastIndex >= 0){
var prependStr = this.value.substring(0, lastIndex);
this.value = prependStr + '#' + ui.item.value + this.value.substr(pos + leftovers);
comments.init.setCaretPosition(this, prependStr.length + ui.item.value.length + 1);
}
return false;
}
I changed the select function a bit to cover the leftovers. Now, it's searching for the next " " occurrence and adds the length of everything before it to the replaced value.
Hope this helps :)

Categories

Resources