summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/widgets/auto_complete.js
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web/static/src/js/widgets/auto_complete.js')
-rw-r--r--addons/web/static/src/js/widgets/auto_complete.js220
1 files changed, 220 insertions, 0 deletions
diff --git a/addons/web/static/src/js/widgets/auto_complete.js b/addons/web/static/src/js/widgets/auto_complete.js
new file mode 100644
index 00000000000..883206ae4c2
--- /dev/null
+++ b/addons/web/static/src/js/widgets/auto_complete.js
@@ -0,0 +1,220 @@
+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;
+ },
+});
+});