I am using a JsonReader to map Json data to variables to be used in a grid/form. The back end is in Java and there are complex objects which I Jsonify and pass to the ExtJS front end.
This is a part of my JsonReader which tries to retrieve a nested object -
{name:'status', type: 'string', mapping: 'status.name'}
This works fine when status has a value (not null in the server), but the grid load fails when status is null. Currently the work around I have is to send an empty object from the server in case of null, but I assume there should be a way to handle this in ExtJS. Please suggest a better solution on the ExtJS side.
I can think of two possibilities - one documented and one undocumented:
use the convert()-mechanism of Ext.data.Field:
{
name:'status',
mapping: 'status',
convert: function(status, data) {
if (!Ext.isEmpty(status) && status.name) {
return status.name;
} else {
return null;
}
}
}
The mapping property can also take an extractor function (this is undocumented so perhaps it may be a little bit risky to rely on this):
{
name:'status',
mapping: function(data) {
if (data.status && data.status.name) {
return data.status.name;
} else {
return null;
}
}
}
Use this safe json reader instead:
Ext.define('Ext.data.reader.SafeJson', {
extend: 'Ext.data.reader.Json',
alias : 'reader.safe',
/**
* #private
* Returns an accessor function for the given property string. Gives support for properties such as the following:
* 'someProperty'
* 'some.property'
* 'some["property"]'
* This is used by buildExtractors to create optimized extractor functions when casting raw data into model instances.
*/
createAccessor: function() {
var re = /[\[\.]/;
return function(expr) {
if (Ext.isEmpty(expr)) {
return Ext.emptyFn;
}
if (Ext.isFunction(expr)) {
return expr;
}
if (this.useSimpleAccessors !== true) {
var i = String(expr).search(re);
if (i >= 0) {
if (i > 0) { // Check all property chain for existence. Return null if any level does not exist.
var a = [];
var l = expr.split('.');
var r = '';
for (var w in l) {
r = r + '.' + l[w];
a.push('obj' + r);
}
var v = "(" + a.join(" && ") + ") ? obj." + expr + " : null";
return Ext.functionFactory('obj', 'return (' + v + ')');
} else {
return Ext.functionFactory('obj', 'return obj' + expr);
}
}
}
return function(obj) {
return obj[expr];
};
};
}()
});
I have changed Slava Nadvorny's example so that it completely works for ExtJS 4.1.1.
New extended class of Ext.data.reader.Json is below:
Ext.define('Ext.data.reader.SafeJson', {
extend: 'Ext.data.reader.Json',
alias : 'reader.safejson',
/**
* #private
* Returns an accessor function for the given property string. Gives support for properties such as the following:
* 'someProperty'
* 'some.property'
* 'some["property"]'
* This is used by buildExtractors to create optimized extractor functions when casting raw data into model instances.
*/
createAccessor: (function() {
var re = /[\[\.]/;
return function(expr) {
if (Ext.isEmpty(expr)) {
return Ext.emptyFn;
}
if (Ext.isFunction(expr)) {
return expr;
}
if (this.useSimpleAccessors !== true) {
var i = String(expr).search(re);
if (i >= 0) {
if (i > 0) { // Check all property chain for existence. Return null if any level does not exist.
var a = [];
var l = expr.split('.');
var r = '';
for (var w in l) {
r = r + '.' + l[w];
a.push('obj' + r);
}
var v = "(" + a.join(" && ") + ") ? obj." + expr + " : null";
return Ext.functionFactory('obj', 'return (' + v + ')');
} else {
return Ext.functionFactory('obj', 'return obj' + (i > 0 ? '.' : '') + expr);
}
}
}
return function(obj) {
return obj[expr];
};
};
}()),
/**
* #private
* #method
* Returns an accessor expression for the passed Field. Gives support for properties such as the following:
*
* - 'someProperty'
* - 'some.property'
* - 'some["property"]'
*
* This is used by buildExtractors to create optimized on extractor function which converts raw data into model instances.
*/
createFieldAccessExpression: (function() {
var re = /[\[\.]/;
return function(field, fieldVarName, dataName) {
var me = this,
hasMap = (field.mapping !== null),
map = hasMap ? field.mapping : field.name,
result,
operatorSearch;
if (typeof map === 'function') {
result = fieldVarName + '.mapping(' + dataName + ', this)';
} else if (this.useSimpleAccessors === true || ((operatorSearch = String(map).search(re)) < 0)) {
if (!hasMap || isNaN(map)) {
// If we don't provide a mapping, we may have a field name that is numeric
map = '"' + map + '"';
}
result = dataName + "[" + map + "]";
} else {
if (operatorSearch > 0) {
var a = [];
var l = map.split('.');
var r = '';
for (var w in l) {
r = r + '.' + l[w];
a.push(dataName + r);
}
result = "("+a.join(" && ")+") ? "+dataName+"."+map+" : null";
} else {
result = dataName + map;
}
}
return result;
};
}())
});
So you can successfully processing nested JSON-data with null nodes.
Example of JSON:
{
root: [{
id: 1,
name: {
name: "John",
phone: "123"
},
},
{
id: 4,
name: null,
},
]
}
Working example with test data you can find here:
http://jsfiddle.net/8Ftag/
Related
when the server sends a restrictive Content-Security-Policy header,
Content-Security-Policy: default-src 'self'; script-src 'self'; img-src 'self'
the following error comes up in Chrome :
Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'self'".
Strict Content-Security-Policy does not allow eval-like mechanisms, unless the 'unsafe-eval' keyword is specified.
Do you have any tricks or overrides to make your ExtJS application strict CSP compatible ?
The first culprit is the following code, where a Function object is being created:
getInstantiator: function(length) {
var instantiators = this.instantiators,
instantiator,
i,
args;
instantiator = instantiators[length];
if (!instantiator) {
i = length;
args = [];
for (i = 0; i < length; i++) {
args.push('a[' + i + ']');
}
instantiator = instantiators[length] = new Function('c', 'a', 'return new c(' + args.join(',') + ')'); //// CSP PB HERE
//<debug>
instantiator.name = "Ext.create" + length;
//</debug>
}
return instantiator;
},
"new Function" is used also here :
makeInitializeFn: function (cls) {
var code = ['var '],
body = ['\nreturn function (e) {\n var data = e.data, v;\n'],
work = 0,
bc, ec, // == beginClone, endClone
convert, expr, factory, field, fields, fs, hasDefValue, i, length;
if (!(fields = cls.rankedFields)) {
// On the first edit of a record of this type we need to ensure we have the
// topo-sort done:
fields = cls.rankFields();
}
for (i = 0, length = fields.length; i < length; ++i) {
// The generated method declares vars for each field using "f0".."fN' as the
// name. These are used to access properties of the field (e.g., the convert
// method or defaultValue).
field = fields[i];
fs = 'f' + i;
convert = field.convert;
if (i) {
code.push(', \n ');
}
code.push(fs, ' = $fields[' + i + ']');
//<debug>
// this can be helpful when debugging (at least in Chrome):
code.push(' /* ', field.name, ' */');
//</debug>
// NOTE: added string literals are "folded" by the compiler so we
// are better off doing an "'foo' + 'bar'" then "'foo', 'bar'". But
// for variables we are better off pushing them into the array for
// the final join.
if ((hasDefValue = (field.defaultValue !== undefined)) || convert) {
// For non-calculated fields that have some work required (a convert method
// and/or defaultValue), generate a chunk of logic appropriate for the
// field.
//expr = data["fieldName"];
expr = 'data["' + field.name + '"]';
++work;
bc = ec = '';
if (field.cloneDefaultValue) {
bc = 'Ext.clone(';
ec = ')';
}
body.push('\n');
if (convert && hasDefValue) {
// v = data.fieldName;
// if (v !== undefined) {
// v = f2.convert(v, e);
// }
// if (v === undefined) {
// v = f2.defaultValue;
// // or
// v = Ext.clone(f2.defaultValue);
// }
// data.fieldName = v;
//
body.push(' v = ', expr, ';\n' +
' if (v !== undefined) {\n' +
' v = ', fs, '.convert(v, e);\n' +
' }\n' +
' if (v === undefined) {\n' +
' v = ', bc, fs, '.defaultValue',ec,';\n' +
' }\n' +
' ', expr, ' = v;');
} else if (convert) { // no defaultValue
// v = f2.convert(data.fieldName,e);
// if (v !== undefined) {
// data.fieldName = v;
// }
//
body.push(' v = ', fs, '.convert(', expr, ',e);\n' +
' if (v !== undefined) {\n' +
' ', expr, ' = v;\n' +
' }\n');
} else if (hasDefValue) { // no convert
// if (data.fieldName === undefined) {
// data.fieldName = f2.defaultValue;
// // or
// data.fieldName = Ext.clone(f2.defaultValue);
// }
//
body.push(' if (', expr, ' === undefined) {\n' +
' ', expr, ' = ',bc,fs,'.defaultValue',ec,';\n' +
' }\n');
}
}
}
if (!work) {
// There are no fields that need special processing
return Ext.emptyFn;
}
code.push(';\n');
code.push.apply(code, body);
code.push('}');
code = code.join('');
// Ensure that Ext in the function code refers to the same Ext that we are using here.
// If we are in a sandbox, global.Ext might be different.
factory = new Function('$fields', 'Ext', code); /// CSP PROBLEM HERE
return factory(fields, Ext);
}
} // static
} // privates
},
This policy prevents new Function(), which rely upon for a performance optimisation in ExtJS, I suppose.
The policy prevents also the use of "eval".
Ext.JSON = (new(function() {
// #define Ext.JSON
// #require Ext
// #require Ext.Error
var me = this,
hasNative = window.JSON && JSON.toString() === '[object JSON]',
useHasOwn = !! {}.hasOwnProperty,
pad = function(n) {
return n < 10 ? "0" + n : n;
},
doDecode = function(json) {
return eval("(" + json + ')'); // jshint ignore:line //////CSP PROBLEM
},
...
// in Template.js
evalCompiled: function($) {
// We have to use eval to realize the code block and capture the inner func we also
// don't want a deep scope chain. We only do this in Firefox and it is also unhappy
// with eval containing a return statement, so instead we assign to "$" and return
// that. Because we use "eval", we are automatically sandboxed properly.
eval($); // jshint ignore:line
return $;
},
//in XTemplateCompiler
evalTpl: function ($) {
// We have to use eval to realize the code block and capture the inner func we also
// don't want a deep scope chain. We only do this in Firefox and it is also unhappy
// with eval containing a return statement, so instead we assign to "$" and return
// that. Because we use "eval", we are automatically sandboxed properly.
eval($);
return $;
},
// in Managet.js
privates: {
addProviderClass: function(type, cls) {
this.providerClasses[type] = cls;
},
onApiLoadSuccess: function(options) {
var me = this,
url = options.url,
varName = options.varName,
api, provider, error;
try {
// Variable name could be nested (default is Ext.REMOTING_API),
// so we use eval() to get the actual value.
api = Ext.apply(options.config, eval(varName)); ////////CSP
provider = me.addProvider(api);
}
// in Ext.dom.Query
eval("var batch = 30803, child, next, prev, byClassName;");
//...
eval(fn.join(""));
I have a JSON file with list of employes. I imported this data to table. Next step is add sorting by <th>. My script doesn't work and nothing happens. I don't know the reason. I'm just a JS beginner. Can You help me why sorting is not working?
Here is my repository:
https://github.com/rrajca/Employee-table
My js is:
$(document).ready(function() {
$.getJSON("dane.json", function(data) {
/* var sortedList = data.sort(function(a, b) {
return a.id - b.id;
}) */
var employeeList = "";
$.each(data, function(key, value) {
employeeList += "<tr>";
employeeList += "<td>"+value.id+"</td>";
employeeList += "<td>"+value.firstName+"</td>";
employeeList += "<td>"+value.lastName+"</td>";
employeeList += "<td>"+value.dateOfBirth+"</td>";
employeeList += "<td>"+value.company+"</td>";
employeeList += "<td>"+value.note+"</td>";
employeeList += "</tr>";
})
$("tbody").append(employeeList);
})
var compare = {
name: function(a, b) {
a = a.replace(/^the /i, '');
b = b.replace(/^the /i, '');
if (a < b) {
return -1;
} else {
return a > b ? 1 : 0;
}
},
duration: function(a, b) {
a = a.split(':');
b = b.split(':');
a = Number(a[0]) * 60 + Number(a[1]);
b = Number(b[0]) * 60 + Number(b[1]);
return a - b;
},
date: function(a, b) {
a = new Date(a);
b = new Date(b);
return a - b;
}
};
$('.sortable').each(function() {
var $table = $(this);
var $tbody = $table.find('tbody');
var $controls = $table.find('th');
var rows = $tbody.find('tr').toArray();
$controls.on('click', function() {
var $header = $(this);
var order = $header.data('sort');
var column;
if ($header.is('.ascending') || $header.is('.descending')) {
$header.toggleClass('ascending descending');
$tbody.append(rows.reverse());
} else {
$header.addClass('ascending');
$header.siblings().removeClass('ascending descending');
if (compare.hasOwnProperty(order)) {
column = $controls.index(this);
rows.sort(function(a, b) {
a = $(a).find('td').eq(column).text();
b = $(b).find('td').eq(column).text();
return compare[order](a, b);
});
$tbody.append(rows);
}
}
});
});
});
Change your comparison function to:
name: function(a, b) { // Add a method called name
a = a.replace(/^the /i, ''); // Remove The from start of parameter
b = b.replace(/^the /i, ''); // Remove The from start of parameter
if(parseInt(a) == +a) {
a = +a;
b = +b;
}
return a > b
}
The problem was that you were doing string comparisons on everything. So I added in a check to see if it was a number, and casted appropriately.
https://jsfiddle.net/gj2vonsL/7/
The other comparison functions that you wrote have other issues like invalid date formats, etc. But your general idea is correct
I have this:
var addresses_tmp = addresses.slice();
var final_addresses = [];
addresses.forEach(function (current_address, i) {
var near_addresses = [];
near_addresses.push(current_address);
addresses_tmp.forEach(function (next_address, j) {
if (current_address.address.info.code != next_address.address.info.code) {
var data = {
key : $scope.MicrosoftKey,
optmz : "distance",
routeAttributes : "routePath"
}
data["wp.0"] = current_address.address.location.lat + "," + current_address.address.location.lng;
data["wp.1"] = next_address.address.location.lat + "," + next_address.address.location.lng;
ajax.sendApiRequest(data, "GET", "http://dev.virtualearth.net/REST/V1/Routes/Driving", is_url=true).then(
function(response) {
var distance = response.data.resourceSets[0].resources[0].travelDistance;
if (distance < 0.020) {
near_addresses.push(next_address);
addresses_tmp.splice(j, 1);
}
if (j == addresses.length - 1) {
final_addresses.push(near_addresses);
if (near_addresses.length == 1) {
addresses_tmp.splice(j, 1);
}
addresses_tmp.splice(i, 1);
}
// if (count == addresses.length * addresses.length) {
// console.log("ya he acabado todasssss")
// }
},
function(error) {
console.log("error", error);
}
)
}
})
})
And I would like to break all function when the first foreach and the second foreach are finished but I cant put if condition to do this.
As I have ajax inside the second foreach, my variables are crazy so I cant put an if condition to break it.
I need to do this because I am compare two arrays and getting distance between two points (one in the first array and second in the other array)
I'm retrieving data with http.get.
This provides me with an array of objects like below:
[{
"id”:12345,
"resource_state":2,
"external_id”:”abscdgrft”,
"upload_id”:1234567,
"athlete":{
"id”:123456,
"resource_state":2,
"firstname”:”testname”,
"lastname”:”testlastname”,
"profile_medium”:”image/athlete/medium.png",
"profile":"image/athlete/large.png",
"city”:”testcity”,
"state”:”teststate”,
…
},
"name”:”test name“,
"distance":87223.1,
"moving_time":11026,
"elapsed_time":11173,
"total_elevation_gain":682.3,
…
}]
I would like to combine all those object based on the athlete.firstname + athlete.lastname.
So for example all objects with athlete first name Jim and last name Donalds I want to be combined in one object, the same goes for all the other names.
When combining the objects based on the full name, values like "distance", "moving_time", "elapsed_time" and "total_elevation_gain" needs to be summed.
I tried using the code below but the problem is that I can't get it to work with multiple values like I mention above.
This is working only with one value, distance for example:
var obj = {}; // Initialize the object
angular.forEach(data, function(value, key) {
if (value.start_date > firstdayOfWeek && value.start_date < lastdayOfWeek) {
if (obj[value.athlete.firstname + ' ' + value.athlete.lastname]) { // If already exists
obj[value.athlete.firstname + ' ' + value.athlete.lastname] += value.distance; // Add value to previous value
} else {
obj[value.athlete.firstname + ' ' + value.athlete.lastname] = value.distance; // Add in object
}
} else {
//do nothing
}
});
console.log(obj); // Result
When I modify it like this it is not working anymore.
var obj = {};
angular.forEach(data, function(value, key) {
//console.log(value);
if (value.start_date > startOfLastWeek && value.start_date < endOfLastWeek) {
//console.log(value);
if (obj[value.athlete.firstname + ' ' + value.athlete.lastname]) { // If already exists
obj[value.athlete.firstname + ' ' + value.athlete.lastname] += {
"profile" : value.athlete.profile,
"distance" : value.distance,
"moving_time" : value.moving_time,
"elapsed_time" : value.elapsed_time,
"total_elevation_gain" : value.total_elevation_gain,
}; // Add value to previous value
} else {
obj[value.athlete.firstname + ' ' + value.athlete.lastname] = {
"profile" : value.athlete.profile,
"distance" : value.distance,
"moving_time" : value.moving_time,
"elapsed_time" : value.elapsed_time,
"total_elevation_gain" : value.total_elevation_gain,
}; // Add in object
}
} else {
//do nothing
}
});
console.log(obj); // Result
Thanks!
try to add item by item... You can't add all with += { ... }:
var obj = {};
angular.forEach(data, function(value, key) {
//console.log(value);
if (value.start_date > startOfLastWeek && value.start_date < endOfLastWeek) {
//console.log(value);
if (obj[value.athlete.firstname + ' ' + value.athlete.lastname]) { // If already exists
var aux = obj[value.athlete.firstname + ' ' + value.athlete.lastname];
aux.profile += value.athlete.profile;
aux.distance += value.distance;
...
} else {
obj[value.athlete.firstname + ' ' + value.athlete.lastname] = {
"profile" : value.athlete.profile,
"distance" : value.distance,
"moving_time" : value.moving_time,
"elapsed_time" : value.elapsed_time,
"total_elevation_gain" : value.total_elevation_gain,
}; // Add in object
}
} else {
//do nothing
}
});
console.log(obj); // Result
Use underscore or lodash groupBy, map and reduce
with lodash:
_.chain(myArr).map(function(o) {
return {
fullname: o.athlete.firstname + ' ' + o.athlete.lastname,
distance: o.distance,
moving_time: o.moving_time,
elapsed_time: o.elapsed_time,
total_elevation_gain: o.total_elevation_gain
}
}).groupBy(function(o) {
return o.fullaname
}).map(function(d, fullname) {
var totals = _.reduce(d, function(result, run, n) {
result.moving_time += run.moving_time | 0
result.elapsed_time += run.elapsed_time | 0
result.total_elevation_gain += run.total_elevation_gain | 0
return result
}, {
moving_time: 0,
elapsed_time: 0,
total_elevation_gain: 0
})
totals.fullname = fullname
return totals
})
I am using Bootstrap Typeahead along with Backbone to display data fetched from the server based on user input. The backing Backbone collection, which is used to store the fetched data, is created once on app startup. The collection is re-used for every new search in Typeahead.
The issue I am having is the browser memory usage keeps going up every time users searches for something. My question is really how can I make sure the old result/data in collection is garbage collected when the new one comes in? Also is re-using the same collection for new search the right way to go?
The collection js file
define([
"data",
"backbone",
"vent"
],
function (data, Backbone, vent) {
var SearchCollection = Backbone.Collection.extend({
model:Backbone.Model,
url:function () {
return data.getUrl("entitysearch");
},
initialize:function (models, options) {
var self=this;
this.requestAborted = false;
this.categories = (options && options.categories) || ['counterparty', 'company', 'user'];
this.onItemSelected = options.onItemSelected;
this.selectedId = options.selectedId; // should be prefixed with type eg. "company-12345"
_.bindAll(this, "entitySelected");
vent.bindTo(vent, "abortSearchAjax", function () {
this.requestAborted = true;
}, this);
},
search:function (criteria) {
var self = this,
results = [];
// abort any existing requests
if (this.searchRequest) {
this.searchRequest.abort();
}
self.requestAborted= false;
this.searchRequest = this.fetch({
data:$.param({
query:criteria,
types: this.mapEntityTypesToCodes(this.categories),
fields:'ANY',
max: 500
})
})
.done(function(response, textStatus, jqXHR) {
if (!self.requestAborted){
results = self.processResponse(response);
}
})
.fail(function(jqXHR, textStatus, errorThrown) {
if(errorThrown === "Unauthorized" || errorThrown === "Forbidden") {
alert("Either you do not have the right permissions to search for entities or you do not have a valid SSO token." +
" Reload the page to update your SSO token.");
}
})
.always(function(){
if (!self.requestAborted){
self.reset(results);
self.trigger('searchComplete');
}
});
},
/**
* Backbone parse won't work here as it requires you to modify the original response object not create a new one.
* #param data
* #return {Array}
*/
processResponse:function (response) {
var self = this,
result = [];
_.each(response, function (val, key, list) {
if (key !== 'query') {
_.map(val, function (v, k, l) {
var id;
v.type = self.mapEntityShortName(key);
id = v.id;
v.id = v.type + '-' + v.id;
v.displayId = id;
});
result = result.concat(val);
}
});
return result;
},
mapEntityTypesToCodes:function (types) {
var codes = [],
found = false;
_.each(types, function(el, index, list) {
{
switch (el) {
case 'counterparty':
codes.push('L5');
found = true;
break;
case 'company':
codes.push('L3');
found = true;
break;
case 'user':
codes.push('user');
found = true;
break;
}
}
});
if (!found) {
throw "mapEntityTypesToCodes - requires an array containing one or more types - counterparty, company, user";
}
return codes.join(',');
},
mapEntityShortName: function(name) {
switch (name) {
case 'parties':
return 'counterparty';
break;
case 'companies':
return 'company';
break;
case 'users':
return 'user';
break;
}
},
entitySelected:function (item) {
var model,
obj = JSON.parse(item),
data;
model = this.get(obj.id);
if (model) {
model.set('selected', true);
data = model.toJSON();
this.selectedId = obj.id;
//correct the id to remove the type eg. company-
data.id = data.displayId;
this.onItemSelected && this.onItemSelected(data);
} else {
throw "entitySelected - model not found";
}
},
openSelectedEntity: function() {
var model = this.get(this.selectedId);
if (model) {
vent.trigger('entityOpened', {
id: this.selectedId.split('-')[1],
name: model.get('name'),
type: model.get('type')
});
}
},
entityClosed:function (id) {
var model;
model = this.where({
id:id
});
if (model.length) {
model[0].set('selected', false);
}
}
});
return SearchCollection;
});
The View js file
define([
"backbone",
"hbs!modules/search/templates/search",
"bootstrap-typeahead"
],
function (Backbone, tpl) {
return Backbone.Marionette.ItemView.extend({
events: {
'click .action-open-entity': 'openEntity'
},
className: 'modSearch',
template:{
type:'handlebars',
template:tpl
},
initialize:function (options) {
_.bindAll(this, "render", "sorter", "renderSearchResults", "typeaheadSource");
this.listen();
this.categoryNames = options.categoryNames;
this.showCategoryNames = options.showCategoryNames;
this.autofocus = options.autofocus;
this.initValue = options.initValue;
this.disabled = options.disabled;
this.updateValueOnSelect = options.updateValueOnSelect;
this.showLink = options.showLink;
this.resultsLength = 1500;
},
listen:function () {
this.collection.on('searchComplete', this.renderSearchResults);
this.collection.on('change:selected', this.highlightedItemChange, this);
},
resultsFormatter:function () {
var searchResults = [],
that = this;
this.collection.each(function (result) {
searchResults.push(that._resultFormatter(result));
});
return searchResults;
},
_resultFormatter:function (model) {
var result = {
name:model.get('name'),
id:model.get('id'),
displayId: model.get('displayId'),
aliases:model.get('aliases'),
type:model.get('type'),
marketsPriority:model.get('marketsPriority')
};
if (model.get('ssoId')) {
result.ssoId = model.get('ssoId');
}
return JSON.stringify(result);
},
openEntity: function() {
this.collection.openSelectedEntity();
},
serializeData:function () {
return {
categoryNames:this.categoryNames,
showCategoryNames: this.showCategoryNames,
initValue:this.initValue,
autofocus:this.autofocus,
showLink:this.showLink
};
},
onRender:function () {
var self = this,
debouncedSearch;
if (this.disabled === true) {
this.$('input').attr('disabled', 'disabled');
} else {
debouncedSearch = _.debounce(this.typeaheadSource, 500);
this.typeahead = this.$('.typeahead')
.typeahead({
source: debouncedSearch,
categories:{
'counterparty':'Counterparties',
'company':'Companies',
'user':'Users'
},
minLength:3,
multiSelect:true,
items:this.resultsLength,
onItemSelected:self.collection.entitySelected,
renderItem:this.renderDropdownItem,
matcher: this.matcher,
sorter:this.sorter,
updateValueOnSelect:this.updateValueOnSelect
})
.data('typeahead');
$('.details').hide();
}
},
onClose: function(){
this.typeahead.$menu.remove();
},
highlightedItemChange:function (model) {
this.typeahead.changeItemHighlight(model.get('displayId'), model.get('selected'));
},
renderSearchResults:function () {
this.searchCallback(this.resultsFormatter());
},
typeaheadSource:function (query, searchCallback) {
this.searchCallback = searchCallback;
this.collection.search(query);
},
/**
* Called from typeahead plugin
* #param item
* #return {String}
*/
renderDropdownItem:function (item) {
var entity,
marketsPriority = '',
aliases = '';
if (!item) {
return item;
}
if (typeof item === 'string') {
entity = JSON.parse(item);
if (entity.marketsPriority && (entity.marketsPriority === "Y")) {
marketsPriority = '<span class="marketsPriority">M</span>';
}
if (entity.aliases && (entity.aliases.constructor === Array) && entity.aliases.length) {
aliases = ' (' + entity.aliases.join(', ') + ') ';
}
if (entity.type === "user"){
entity.displayId = entity.ssoId;
}
return [entity.name || '', aliases, ' (', entity.displayId || '', ')', marketsPriority].join('');
}
return item;
},
matcher: function(item){
return item;
},
/**
* Sort typeahead results - called from typeahead plugin
* #param items
* #param query
* #return {Array}
*/
sorter:function (items, query) {
var results = {},
reducedResults,
unmatched,
filteredItems,
types = ['counterparty', 'company', 'user'],
props = ['displayId', 'name', 'aliases', 'ssoId'],
type,
prop;
query = $.trim(query);
for (var i = 0, j = types.length; i < j; i++) {
type = types[i];
filteredItems = this._filterByType(items, type);
for (var k = 0, l = props.length; k < l; k++) {
prop = props[k];
unmatched = [];
if (!results[type]) {
results[type] = [];
}
results[type] = results[type].concat(this._filterByProperty(query, filteredItems, prop, unmatched));
filteredItems = unmatched;
}
}
reducedResults = this._reduceItems(results, types, this.resultsLength);
return reducedResults;
},
/**
* Sort helper - match query string against a specific property
* #param query
* #param item
* #param fieldToMatch
* #param resultArrays
* #return {Boolean}
* #private
*/
_matchProperty:function (query, item, fieldToMatch, resultArrays) {
if (fieldToMatch.toLowerCase().indexOf(query.toLowerCase()) === 0) {
resultArrays.beginsWith.push(item);
} else if (~fieldToMatch.indexOf(query)) resultArrays.caseSensitive.push(item)
else if (~fieldToMatch.toLowerCase().indexOf(query.toLowerCase())) resultArrays.caseInsensitive.push(item)
else if(this._fieldConatins(query, fieldToMatch, resultArrays)) resultArrays.caseInsensitive.push(item)
else return false;
return true;
},
_fieldConatins:function (query, fieldToMatch, resultArrays) {
var matched = false;
var queryList = query.split(" ");
_.each(queryList, function(queryItem) {
if(fieldToMatch.toLowerCase().indexOf(queryItem.toLowerCase()) !== -1) {
matched = true;
return;
}
});
return matched;
},
/**
* Sort helper - filter result set by property type (name, id)
* #param query
* #param items
* #param prop
* #param unmatchedArray
* #return {Array}
* #private
*/
_filterByProperty:function (query, items, prop, unmatchedArray) {
var resultArrays = {
beginsWith:[],
caseSensitive:[],
caseInsensitive:[],
contains:[]
},
itemObj,
item,
isMatched;
while (item = items.shift()) {
itemObj = JSON.parse(item);
isMatched = itemObj[prop] && this._matchProperty(query, item, itemObj[prop].toString(), resultArrays);
if (!isMatched && unmatchedArray) {
unmatchedArray.push(item);
}
}
return resultArrays.beginsWith.concat(resultArrays.caseSensitive, resultArrays.caseInsensitive, resultArrays.contains);
},
/**
* Sort helper - filter result set by entity type (counterparty, company, user)
* #param {Array} items
* #param {string} type
* #return {Array}
* #private
*/
_filterByType:function (items, type) {
var item,
itemObj,
filtered = [];
for (var i = 0, j = items.length; i < j; i++) {
item = items[i];
itemObj = JSON.parse(item);
if (itemObj.type === type) {
filtered.push(item);
}
}
return filtered;
},
/**
* Sort helper - reduce the result set down and split between the entity types (counterparty, company, user)
* #param results
* #param types
* #param targetLength
* #return {Array}
* #private
*/
_reduceItems:function (results, types, targetLength) {
var categoryLength,
type,
len,
diff,
reduced = [],
reducedEscaped = [];
categoryLength = Math.floor(targetLength / types.length);
for (var i = 0, j = types.length; i < j; i++) {
type = types[i];
len = results[type].length;
diff = categoryLength - len;
if (diff >= 0) { // actual length was shorter
reduced = reduced.concat(results[type].slice(0, len));
categoryLength = categoryLength + Math.floor(diff / (types.length - (i + 1)));
} else {
reduced = reduced.concat(results[type].slice(0, categoryLength));
}
}
_.each(reduced, function(item) {
item = item.replace(/\'/g,"`");
reducedEscaped.push(item);
});
return reducedEscaped;
}
});
});