Skip to content

Instantly share code, notes, and snippets.

@Deraen
Last active January 2, 2016 19:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Deraen/8350730 to your computer and use it in GitHub Desktop.
Save Deraen/8350730 to your computer and use it in GitHub Desktop.
AngularJS Selectize directive, autocomplete. CHECK https://gist.github.com/Deraen/9811993 FOR BETTER VERSION!
angular.module('frontendApp')
.directive('autocomplete', function($timeout, $compile) {
// No use using ng-model or anything as we are only
// interested in CHANGE EVENTS not about current value
// infact, <autocomplete ng-model="foo"> should work as autocomplete is replaced with input element
// => change-callback is optional
return {
restrict: 'E',
replace: true,
template:
'<div class="input-group">' +
' <span class="input-group-addon"><span class="glyphicon glyphicon-search"></span></span>' +
' <input/>' +
'</div>',
link: function(scope, el, attrs, ctrls, transcludeFn) {
var filter = scope.$eval(attrs.filter);
var selectedCb = scope.$eval(attrs.cb);
// attrs.load contains a function which should return a promise
// or a Array which contains the data for autocomplete
var load = scope.$eval(attrs.load);
var map = scope.$eval(attrs.map);
var loaded = false;
var opts = {
maxItems: 1,
valueField: '_id',
labelField: 'name',
sortField: 'name',
searchField: ['name'],
// Exec load even without query
preload: true,
load: function(q, cb) {
// We do all filtering on client side, no need to request data again
if (loaded) return;
if (_.isFunction(load)) {
load().success(function(data) {
if (_.isFunction(map)) {
data = _.map(data, map);
}
cb(data);
loaded = true;
}).error(function() {
cb();
});
} else if (_.isArray(load)) {
if (_.isFunction(map)) {
load = _.map(load, map);
}
cb(load);
} else {
cb();
}
},
onChange: function(val) {
if (_.isFunction(selectedCb)) {
selectedCb(val);
// Deselect
selectize.clear();
// Unfocus field, because after clear dropdown is closed but if clicking
// again on field dropdown wont open as field is already active.
// This way user can click on field and dropdown will open.
selectize.blur();
}
},
render: {},
};
// Couldn't devise another way...
// $compile can't really be used as it creates A ELEMENT
// which will have data bound and real values inserted later
// and render functions have to return A STRING
var templates = {
skill: {
option:
'<div>' +
' <div class="image"><img src="/api/images/skill/<%= d._id %>" class="skill-logo-small"/></div>' +
' <span class="name"><%= d.name %></span>' +
' <span class="tags"><% _.each(d.tags, function(tag) { %><span class="tag"><%= tag.name %></span><% }); %></span>' +
'</div>',
},
project: {
option:
'<div>' +
' <span class="name"><%= d.name %></span>' +
// Argh
'<% if (d.begin) { %><span class="d.begin"><%= moment(d.begin).format("DD.MM.YYYY") %></span> <% if (d.ended) { %> - <span class="ended"><%= moment(d.ended).format("DD.MM.YYYY") %><% } %></span><% } %>' +
'</div>',
},
user: {
option:
'<div class="user">' +
' <span class="name"><%= d.name %></span>' +
' <span class="title"><%= d.title %></span>' +
'</div>',
},
};
if (templates[attrs.template] && templates[attrs.template].option) {
var option = _.template(templates[attrs.template].option);
opts.render.option = function(item) {
return option({d: item});
};
}
// Score func
var scores = {
skill: function(search) {
var score = this.getScoreFunction(search);
return function(item) {
var scoreTags = this.sifter.getScoreFunction(search, {
fields: 'name',
conjunction: this.settings.searchConjunction,
});
var tagsSum = _.reduce(item.tags, function(sum, tag) {
return sum + scoreTags(tag);
}, 0);
return 10 * score(item) + tagsSum;
}.bind(this);
}.bind(this),
};
if (scores[attrs.score]) {
opts.score = scores[scope[attrs.score]];
}
// Init
var $select = $('input', el).selectize(opts);
var selectize = $select[0].selectize;
// WARNING: Monkey-patch selectize.search function
// Use custom filter function to hide options which we don't want to display
// If this breaks in future version of Selectize.js, check search function:
// https://github.com/brianreavis/selectize.js/blob/master/src/selectize.js#L873 (or somewhere)
// and try to mimic what it does with hideSelected
var orgSearch = selectize.search.bind(selectize);
selectize.search = function(query) {
// Disable query cache - who needs performance?
// It would break complete after item is deleted from list (filter needs to run again)
delete this.lastQuery;
var result = orgSearch(query);
if (_.isFunction(filter)) {
var i;
for (i = result.items.length - 1; i >= 0; i--) {
var item = this.options[result.items[i].id];
if (!filter(item)) {
result.items.splice(i, 1);
}
}
}
return result;
}.bind(selectize);
}
};
})
.directive('editableComplete', function(editableDirectiveFactory) {
return editableDirectiveFactory({
directiveName: 'editableComplete',
inputTpl: '<autocomplete class="form-control"/>',
});
});
// Selectize less doesn't seem to support bootstrap large inputs, this might work
.autocomplete {
.selectize-control {
height: @input-height-large;
}
.selectize-input {
.input-lg;
}
.input-group {
.selectize-control:not(:first-child):not(:last-child) .selectize-input {
border-radius: 0;
}
.selectize-control:last-child .selectize-input {
.border-right-radius(0);
}
.selectize-control:first-child .selectize-input {
.border-left-radius(0);
}
}
input {
font-size: 18px;
width: auto !important;
}
// Template specific
.image {
display: inline-block;
width: 60px;
}
.tags {
margin-left: 20px;
.tag {
.skill-tag;
font-size: 12px;
opacity: 0.7;
}
}
.user {
.name {
font-weight: bold;
}
.title {
margin-left: 20px;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment