221 lines
7.8 KiB
JavaScript
221 lines
7.8 KiB
JavaScript
|
odoo.define('web.AutoComplete', function (require) {
|
||
|
"use strict";
|
||
|
|
||
|
var Widget = require('web.Widget');
|
||
|
|
||
|
return Widget.extend({
|
||
|
template: "SearchView.autocomplete",
|
||
|
|
||
|
// Parameters for autocomplete constructor:
|
||
|
//
|
||
|
// parent: this is used to detect keyboard events
|
||
|
//
|
||
|
// options.source: function ({term:query}, callback). This function will be called to
|
||
|
// obtain the search results corresponding to the query string. It is assumed that
|
||
|
// options.source will call callback with the results.
|
||
|
// options.select: function (ev, {item: {facet:facet}}). Autocomplete widget will call
|
||
|
// that function when a selection is made by the user
|
||
|
// options.get_search_string: function (). This function will be called by autocomplete
|
||
|
// to obtain the current search string.
|
||
|
init: function (parent, options) {
|
||
|
this._super(parent);
|
||
|
this.$input = parent.$el;
|
||
|
this.source = options.source;
|
||
|
this.select = options.select;
|
||
|
this.get_search_string = options.get_search_string;
|
||
|
|
||
|
this.current_result = null;
|
||
|
|
||
|
this.searching = true;
|
||
|
this.search_string = '';
|
||
|
this.current_search = null;
|
||
|
},
|
||
|
start: function () {
|
||
|
var self = this;
|
||
|
this.$input.on('keyup', function (ev) {
|
||
|
if (ev.which === $.ui.keyCode.RIGHT) {
|
||
|
self.searching = true;
|
||
|
ev.preventDefault();
|
||
|
return;
|
||
|
}
|
||
|
if (ev.which === $.ui.keyCode.ENTER) {
|
||
|
if (self.search_string.length) {
|
||
|
self.select_item(ev);
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
var search_string = self.get_search_string();
|
||
|
if (self.search_string !== search_string) {
|
||
|
if (search_string.length) {
|
||
|
self.search_string = search_string;
|
||
|
self.initiate_search(search_string);
|
||
|
} else {
|
||
|
self.close();
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
this.$input.on('keypress', function (ev) {
|
||
|
self.search_string = self.search_string + String.fromCharCode(ev.which);
|
||
|
if (self.search_string.length) {
|
||
|
self.searching = true;
|
||
|
var search_string = self.search_string;
|
||
|
self.initiate_search(search_string);
|
||
|
} else {
|
||
|
self.close();
|
||
|
}
|
||
|
});
|
||
|
this.$input.on('keydown', function (ev) {
|
||
|
switch (ev.which) {
|
||
|
case $.ui.keyCode.ENTER:
|
||
|
|
||
|
// TAB and direction keys are handled at KeyDown because KeyUp
|
||
|
// is not guaranteed to fire.
|
||
|
// See e.g. https://github.com/aef-/jquery.masterblaster/issues/13
|
||
|
case $.ui.keyCode.TAB:
|
||
|
if (self.search_string.length) {
|
||
|
self.select_item(ev);
|
||
|
}
|
||
|
break;
|
||
|
case $.ui.keyCode.DOWN:
|
||
|
self.move('down');
|
||
|
self.searching = false;
|
||
|
ev.preventDefault();
|
||
|
break;
|
||
|
case $.ui.keyCode.UP:
|
||
|
self.move('up');
|
||
|
self.searching = false;
|
||
|
ev.preventDefault();
|
||
|
break;
|
||
|
case $.ui.keyCode.RIGHT:
|
||
|
self.searching = false;
|
||
|
var current = self.current_result;
|
||
|
if (current && current.expand && !current.expanded) {
|
||
|
self.expand();
|
||
|
self.searching = true;
|
||
|
}
|
||
|
ev.preventDefault();
|
||
|
break;
|
||
|
case $.ui.keyCode.ESCAPE:
|
||
|
self.close();
|
||
|
self.searching = false;
|
||
|
break;
|
||
|
}
|
||
|
});
|
||
|
},
|
||
|
initiate_search: function (query) {
|
||
|
if (query === this.search_string && query !== this.current_search) {
|
||
|
this.search(query);
|
||
|
}
|
||
|
},
|
||
|
search: function (query) {
|
||
|
var self = this;
|
||
|
this.current_search = query;
|
||
|
this.source({term:query}, function (results) {
|
||
|
if (results.length) {
|
||
|
self.render_search_results(results);
|
||
|
self.focus_element(self.$('li:first-child'));
|
||
|
} else {
|
||
|
self.close();
|
||
|
}
|
||
|
});
|
||
|
},
|
||
|
render_search_results: function (results) {
|
||
|
var self = this;
|
||
|
var $list = this.$el;
|
||
|
$list.empty();
|
||
|
results.forEach(function (result) {
|
||
|
var $item = self.make_list_item(result).appendTo($list);
|
||
|
result.$el = $item;
|
||
|
});
|
||
|
this.show();
|
||
|
},
|
||
|
make_list_item: function (result) {
|
||
|
var self = this;
|
||
|
var $li = $('<li>')
|
||
|
.hover(function () {self.focus_element($li);})
|
||
|
.mousedown(function (ev) {
|
||
|
if (ev.button === 0) { // left button
|
||
|
self.select(ev, {item: {facet: result.facet}});
|
||
|
self.close();
|
||
|
}
|
||
|
})
|
||
|
.data('result', result);
|
||
|
if (result.expand) {
|
||
|
var $expand = $('<a class="o-expand" href="#">').appendTo($li);
|
||
|
$expand.mousedown(function (ev) {
|
||
|
ev.stopPropagation();
|
||
|
ev.preventDefault(); // to prevent dropdown from closing
|
||
|
if (result.expanded) {
|
||
|
self.fold();
|
||
|
} else {
|
||
|
self.expand();
|
||
|
}
|
||
|
});
|
||
|
$expand.click(function(ev) {
|
||
|
ev.preventDefault(); // to prevent url from changing due to href="#"
|
||
|
});
|
||
|
result.expanded = false;
|
||
|
}
|
||
|
if (result.indent) $li.addClass('o-indent');
|
||
|
$li.append($('<a href="#">').html(result.label));
|
||
|
return $li;
|
||
|
},
|
||
|
expand: function () {
|
||
|
var self = this;
|
||
|
var current_result = this.current_result;
|
||
|
current_result.expand(this.get_search_string()).then(function (results) {
|
||
|
(results || [{label: '(no result)'}]).reverse().forEach(function (result) {
|
||
|
result.indent = true;
|
||
|
var $li = self.make_list_item(result);
|
||
|
current_result.$el.after($li);
|
||
|
});
|
||
|
self.current_result.expanded = true;
|
||
|
self.current_result.$el.find('a.o-expand').removeClass('o-expand').addClass('o-expanded');
|
||
|
});
|
||
|
},
|
||
|
fold: function () {
|
||
|
var $next = this.current_result.$el.next();
|
||
|
while ($next.hasClass('o-indent')) {
|
||
|
$next.remove();
|
||
|
$next = this.current_result.$el.next();
|
||
|
}
|
||
|
this.current_result.expanded = false;
|
||
|
this.current_result.$el.find('a.o-expanded').removeClass('o-expanded').addClass('o-expand');
|
||
|
},
|
||
|
focus_element: function ($li) {
|
||
|
this.$('li').removeClass('o-selection-focus');
|
||
|
$li.addClass('o-selection-focus');
|
||
|
this.current_result = $li.data('result');
|
||
|
},
|
||
|
select_item: function (ev) {
|
||
|
if (this.current_result.facet) {
|
||
|
this.select(ev, {item: {facet: this.current_result.facet}});
|
||
|
this.close();
|
||
|
}
|
||
|
},
|
||
|
show: function () {
|
||
|
this.$el.show();
|
||
|
},
|
||
|
close: function () {
|
||
|
this.current_search = null;
|
||
|
this.search_string = '';
|
||
|
this.searching = true;
|
||
|
this.$el.hide();
|
||
|
},
|
||
|
move: function (direction) {
|
||
|
var $next;
|
||
|
if (direction === 'down') {
|
||
|
$next = this.$('li.o-selection-focus').next();
|
||
|
if (!$next.length) $next = this.$('li').first();
|
||
|
} else {
|
||
|
$next = this.$('li.o-selection-focus').prev();
|
||
|
if (!$next.length) $next = this.$('li').last();
|
||
|
}
|
||
|
this.focus_element($next);
|
||
|
},
|
||
|
is_expandable: function () {
|
||
|
return !!this.$('.o-selection-focus .o-expand').length;
|
||
|
},
|
||
|
});
|
||
|
});
|