diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css index 10ca4d89f25..a52cfb22c78 100644 --- a/addons/web/static/src/css/base.css +++ b/addons/web/static/src/css/base.css @@ -1564,6 +1564,46 @@ -webkit-border-radius: 2px; border-radius: 2px; } +.openerp .oe_searchview .oe-autocomplete { + display: none; + position: absolute; + width: 300px; + background-color: white; + border: 1px solid black; + z-index: 666; + margin-top: 5px; + cursor: default; +} +.openerp .oe_searchview .oe-autocomplete ul { + list-style-type: none; + padding-left: 0; + margin: 5px; +} +.openerp .oe_searchview .oe-autocomplete ul li { + padding-left: 20px; + text-shadow: 0 0 0 white; +} +.openerp .oe_searchview .oe-autocomplete ul li span:first-child { + margin-right: 5px; +} +.openerp .oe_searchview .oe-autocomplete ul li span.oe-expand { + cursor: pointer; +} +.openerp .oe_searchview .oe-autocomplete ul li.oe-indent { + margin-left: 20px; +} +.openerp .oe_searchview .oe-autocomplete ul li.oe-selection-focus { + background-color: #7c7bad; + color: white; +} +.openerp .oe_searchview .oe-autocomplete ul li.oe-separator { + margin-top: 2px; + margin-bottom: 2px; + border-top: 1px solid #afafb6; +} +.openerp .oe_searchview .oe-autocomplete ul li.oe-separator:last-child { + display: none; +} .openerp .oe_searchview_drawer_container { overflow: auto; } diff --git a/addons/web/static/src/css/base.sass b/addons/web/static/src/css/base.sass index 5aa1b2ebf50..76e56b249cf 100644 --- a/addons/web/static/src/css/base.sass +++ b/addons/web/static/src/css/base.sass @@ -1289,6 +1289,39 @@ $sheet-padding: 16px text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4) @include radius(2px) + .oe-autocomplete + display: none + position: absolute + width: 300px + background-color: white + border: 1px solid black + z-index: 666 + margin-top: 5px + cursor: default + ul + list-style-type: none + padding-left: 0 + margin: 5px + li + padding-left: 20px + text-shadow: 0 0 0 white + span:first-child + margin-right: 5px + span.oe-expand + cursor: pointer + li.oe-indent + margin-left: 20px + li.oe-selection-focus + background-color: #7c7bad + color: white + li.oe-separator + margin-top: 2px + margin-bottom: 2px + border-top: 1px solid #afafb6 + li.oe-separator:last-child + display: none + + .oe_searchview_drawer_container overflow: auto .oe_searchview_drawer diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js index ac441d8eed6..0ec63b890e9 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -350,7 +350,9 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea e.preventDefault(); break; case $.ui.keyCode.RIGHT: - this.focusFollowing(e.target); + if (!this.autocomplete.is_expandable()) { + this.focusFollowing(e.target); + } e.preventDefault(); break; } @@ -484,53 +486,18 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea * Sets up search view's view-wide auto-completion widget */ setup_global_completion: function () { - var autocomplete = this.$el.autocomplete({ + var self = this; + + this.autocomplete = new instance.web.search.AutoComplete(this, { source: this.proxy('complete_global_search'), select: this.proxy('select_completion'), - focus: function (e) { e.preventDefault(); }, - html: true, - autoFocus: true, - minLength: 1, - delay: 250, - }).data('autocomplete'); - - this.$el.on('input', function () { - this.$el.autocomplete('close'); - }.bind(this)); - - // MonkeyPatch autocomplete instance - _.extend(autocomplete, { - _renderItem: function (ul, item) { - // item of completion list - var $item = $( "
" ) - .data( "item.autocomplete", item ) - .appendTo( ul ); - - if (item.facet !== undefined) { - // regular completion item - if (item.first) { - $item.css('borderTop', '1px solid #cccccc'); - } - return $item.append( - (item.label) - ? $('').html(item.label) - : $('').text(item.value)); - } - return $item.text(item.label) - .css({ - borderTop: '1px solid #cccccc', - margin: 0, - padding: 0, - zoom: 1, - 'float': 'left', - clear: 'left', - width: '100%' - }); - }, - _value: function() { + delay: 0, + get_search_string: function () { return self.$('div.oe_searchview_input').text(); }, + width: this.$el.width(), }); + this.autocomplete.appendTo(this.$el); }, /** * Provide auto-completion result for req.term (an array to `resp`) @@ -546,12 +513,6 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea .value()).then(function () { resp(_(arguments).chain() .compact() - .map(function (completion) { - if (completion.length && completion[0].facet !== undefined) { - completion[0].first = true; - } - return completion; - }) .flatten(true) .value()); }); @@ -579,14 +540,9 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea childBlurred: function () { var val = this.$el.val(); this.$el.val(''); - var complete = this.$el.data('autocomplete'); - if ((val && complete.term === undefined) || complete.previous) { - throw new Error("new jquery.ui version altering implementation" + - " details relied on"); - } - delete complete.term; this.$el.removeClass('oe_focused') .trigger('blur'); + this.autocomplete.close(); }, /** * Call the renderFacets method with the correct arguments. @@ -890,8 +846,10 @@ instance.web.SearchViewDrawer = instance.web.Widget.extend({ filters.push(new instance.web.search.Filter(item, group)); break; case 'group': + self.add_separator(); self.make_widgets(item.children, fields, new instance.web.search.Group(group, 'w', item)); + self.add_separator(); break; case 'field': var field = this.make_field( @@ -907,6 +865,11 @@ instance.web.SearchViewDrawer = instance.web.Widget.extend({ group.push(new instance.web.search.FilterGroup(filters, this)); } }, + + add_separator: function () { + if (!(_.last(this.inputs) instanceof instance.web.search.Separator)) + new instance.web.search.Separator(this); + }, /** * Creates a field for the provided field descriptor item (which comes * from fields_view_get) @@ -1349,6 +1312,15 @@ instance.web.search.Filter = instance.web.search.Input.extend(/** @lends instanc get_context: function () { }, get_domain: function () { }, }); + +instance.web.search.Separator = instance.web.search.Input.extend({ + _in_drawer: false, + + complete: function () { + return {is_separator: true}; + } +}); + instance.web.search.Field = instance.web.search.Input.extend( /** @lends instance.web.search.Field# */ { template: 'SearchView.field', default_operator: '=', @@ -1639,7 +1611,25 @@ instance.web.search.ManyToOneField = instance.web.search.CharField.extend({ this._super(view_section, field, parent); this.model = new instance.web.Model(this.attrs.relation); }, - complete: function (needle) { + + complete: function (value) { + if (_.isEmpty(value)) { return $.when(null); } + var label = _.str.sprintf(_.str.escapeHTML( + _t("Search %(field)s for: %(value)s")), { + field: '' + _.escape(this.attrs.string) + '', + value: '' + _.escape(value) + ''}); + return $.when([{ + label: label, + facet: { + category: this.attrs.string, + field: this, + values: [{label: value, value: value}] + }, + expand: this.expand.bind(this), + }]); + }, + + expand: function (needle) { var self = this; // FIXME: "concurrent" searches (multiple requests, mis-ordered responses) var context = instance.web.pyeval.eval( @@ -1652,13 +1642,12 @@ instance.web.search.ManyToOneField = instance.web.search.CharField.extend({ context: context }).then(function (results) { if (_.isEmpty(results)) { return null; } - return [{label: self.attrs.string}].concat( - _(results).map(function (result) { - return { - label: _.escape(result[1]), - facet: facet_from(self, result) - }; - })); + return _(results).map(function (result) { + return { + label: _.escape(result[1]), + facet: facet_from(self, result) + }; + }); }); }, facet_for: function (value) { @@ -2311,6 +2300,213 @@ instance.web.search.custom_filters = new instance.web.Registry({ 'id': 'instance.web.search.ExtendedSearchProposition.Id' }); +instance.web.search.AutoComplete = instance.web.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.delay: delay in millisecond before calling source. Useful if you don't want + // to make too many rpc calls + // 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.delay = options.delay; + this.select = options.select, + this.get_search_string = options.get_search_string; + this.width = options.width || 400; + + this.current_result = null; + + this.searching = true; + this.search_string = null; + this.current_search = null; + }, + start: function () { + var self = this; + this.$el.width(this.width); + this.$input.on('keyup', function (ev) { + if (ev.which === $.ui.keyCode.RIGHT) { + self.searching = true; + ev.preventDefault(); + return; + } + if (!self.searching) { + self.searching = true; + return; + } + self.search_string = self.get_search_string(); + if (self.search_string.length) { + var search_string = self.search_string; + setTimeout(function () { self.initiate_search(search_string);}, self.delay); + } else { + self.close(); + } + }); + this.$input.on('keydown', function (ev) { + switch (ev.which) { + case $.ui.keyCode.TAB: + case $.ui.keyCode.ENTER: + if (self.get_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.$('ul'); + $list.empty(); + var render_separator = false; + results.forEach(function (result) { + if (result.is_separator) { + if (render_separator) + $list.append($('