Replace typeahead by custom completion based on bootstrap dropdown

review20421002
This commit is contained in:
Cédric Krier
2015-10-02 11:49:23 +02:00
parent 60a5226464
commit 1011453e55
8 changed files with 275 additions and 282 deletions
-1
View File
@@ -19,7 +19,6 @@
"bootstrap": "~3.3.4",
"moment": "~2.10.2",
"eonasdan-bootstrap-datetimepicker": "~4.7.14",
"typeahead.js": "~0.11.1",
"gettext.js": "~0.5.0",
"d3": "~3.5.0",
"c3": "~0.4.10"
-2
View File
@@ -13,14 +13,12 @@ this repository contains the full copyright notices and license terms. -->
<script type="text/javascript" src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="bower_components/moment/min/moment.min.js"></script>
<script type="text/javascript" src="bower_components/eonasdan-bootstrap-datetimepicker/build/js/bootstrap-datetimepicker.min.js"></script>
<script type="text/javascript" src="bower_components/typeahead.js/dist/typeahead.bundle.min.js"></script>
<script type="text/javascript" src="bower_components/gettext.js/dist/gettext.min.js"></script>
<script type="text/javascript" src="bower_components/d3/d3.min.js"></script>
<script type="text/javascript" src="bower_components/c3/c3.min.js"></script>
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="bower_components/eonasdan-bootstrap-datetimepicker/build/css/bootstrap-datetimepicker.min.css">
<link rel="stylesheet" href="bower_components/typeahead.js-bootstrap3.less/typeahead.css">
<link rel="stylesheet" href="bower_components/c3/c3.min.css">
<link rel="stylesheet" href="offcanvas.css">
+165 -65
View File
@@ -2935,74 +2935,174 @@
});
Sao.common.processing = new Sao.common.Processing();
Sao.common.get_completion = function(entry, source, match_selected) {
entry.attr('autocomplete', 'off');
entry.typeahead({
'minLength': 1,
'highlight': true
}, {
'limit': Sao.config.limit,
'name': 'suggestions',
'source': source,
'displayKey': 'rec_name',
}, {
'name': 'actions',
'source': function(query, process) {
var results = [];
if (entry.data('search')) {
results.push({
'id': 'search',
'action': Sao.i18n.gettext('Search...'),
'query': query
});
}
if (entry.data('create')) {
results.push({
'id': 'create',
'action': Sao.i18n.gettext('Create...'),
'query': query
});
}
process(results);
},
'displayKey': 'query',
'templates': {
'suggestion': function(context) {
return '<p>' + context.action + '</p>';
}
}
});
entry.on('typeahead:select', match_selected);
};
Sao.common.update_completion = function(entry, record, field, model,
domain) {
var prm = jQuery.Deferred();
var update = function(search_text, domain) {
if (entry.typeahead('val') != search_text) {
return prm.reject();
}
if (!search_text || !model) {
return prm.reject();
}
if (domain === undefined) {
domain = field.get_domain(record);
}
var context = field.get_context(record);
domain = [['rec_name', 'ilike', '%' + search_text + '%'], domain];
Sao.common.InputCompletion = Sao.class_(Object, {
init: function(el, source, match_selected, format) {
el.wrap('<div class="dropdown"/>');
this.dropdown = el.parent();
this.input = el.find('input').add(el.filter('input')).first();
// bootstrap requires an element with data-toggle
jQuery('<span/>', {
'data-toggle': 'dropdown'
}).appendTo(this.dropdown);
this.menu = jQuery('<ul/>', {
'class': 'dropdown-menu',
'role': 'listbox'
}).appendTo(this.dropdown);
this.separator = jQuery('<li/>', {
'role': 'separator',
'class': 'divider'
}).appendTo(this.menu);
this.separator.hide();
var sao_model = new Sao.Model(model);
var search_prm = sao_model.execute('search_read', [domain, 0,
Sao.config.limit, null, ['rec_name']], context);
search_prm.then(function(results) {
if (entry.typeahead('val') != search_text) {
return prm.reject();
this.source = source;
this.match_selected = match_selected;
this.format = format;
this.action_activated = null;
this._search_text = null;
this.input.on('input', function() {
window.setTimeout(this._input.bind(this), 300,
this.input.val());
}.bind(this));
this.input.keydown(function(evt) {
if (evt.which == Sao.common.DOWN_KEYCODE) {
this.menu.find('a').first().focus();
evt.preventDefault();
} else if (evt.which == Sao.common.ESC_KEYCODE) {
if (this.dropdown.hasClass('open')) {
this.menu.dropdown('toggle');
}
} else if (evt.which == Sao.common.RETURN_KEYCODE) {
if (!this.dropdown.hasClass('open')) {
this.menu.dropdown('toggle');
}
}
prm.resolve(results);
}, prm.reject);
}.bind(this));
this.menu.on('hide.bs.dropdown', function() {
this.input.focus();
}.bind(this));
},
set_actions: function(actions, action_activated) {
if (action_activated !== undefined) {
this.action_activated = action_activated;
}
this.menu.find('li.action').remove();
if (jQuery.isEmptyObject(actions)) {
this.separator.hide();
return;
}
this.separator.show();
actions.forEach(function(action) {
var action_id = action[0];
var content = action[1];
jQuery('<li/>', {
'class': 'action action-' + action_id
}).append(jQuery('<a/>', {
'href': '#'
}).append(this._format_action(content)))
.click(function() {
if (this.action_activated) {
this.action_activated(action_id);
}
this.input.val('');
}.bind(this))
.appendTo(this.menu);
}, this);
},
_format: function(content) {
if (this.format) {
return this.format(content);
}
return content;
},
_format_action: function(content) {
if (this.format_action) {
return this.format_action(content);
}
return content;
},
_input: function(text) {
if (text != this.input.val()) {
return;
}
var prm;
if (this.source instanceof Array) {
prm = jQuery.when(source.filter(function(value) {
return value.toLowerCase().startsWith(text.toLowerCase());
}));
} else {
prm = this.source(text);
}
prm.then(function(values) {
if (text != this.input.val()) {
return;
}
this._set_selection(values);
}.bind(this));
},
_set_selection: function(values) {
if (values === undefined) {
values = [];
}
this.menu.find('li.completion').remove();
values.reverse().forEach(function(value) {
jQuery('<li/>', {
'class': 'completion'
}).append(jQuery('<a/>', {
'href': '#'
}).append(this._format(value)))
.click(function() {
if (this.match_selected) {
this.match_selected(value);
}
this.input.focus();
}.bind(this)).prependTo(this.menu);
}, this);
this.menu.find('a').first().keyup(function(evt) {
if (evt.which == Sao.common.UP_KEYCODE) {
this.input.focus();
}
}.bind(this));
if (!this.input.val()) {
if (this.dropdown.hasClass('open')) {
this.menu.dropdown('toggle');
}
} else {
if (!this.dropdown.hasClass('open')) {
this.menu.dropdown('toggle');
}
}
}
});
Sao.common.get_completion = function(el, source,
match_selected, action_activated) {
var format = function(content) {
return content.rec_name;
};
var completion = new Sao.common.InputCompletion(
el, source, match_selected, format);
completion.set_actions([
['search', Sao.i18n.gettext('Search...')],
['create', Sao.i18n.gettext('Create..')]],
action_activated);
};
var search_text = entry.typeahead('val');
window.setTimeout(update, 300, search_text, domain);
return prm.promise();
Sao.common.update_completion = function(
entry, record, field, model, domain) {
var search_text = entry.val();
if (!search_text || !model) {
return jQuery.when();
}
if (domain === undefined) {
domain = field.get_domain(record);
}
var context = field.get_context(record);
domain = [['rec_name', 'ilike', '%' + search_text + '%'], domain];
var sao_model = new Sao.Model(model);
return sao_model.execute('search_read',
[domain, 0, Sao.config.limit, null, ['rec_name']], context);
};
}());
+25 -47
View File
@@ -347,53 +347,31 @@ var Sao = {};
'placeholder': Sao.i18n.gettext('Search...')
});
this.el.append(this.search_entry);
this.search_entry.typeahead({
'highlight': false
}, {
'limit': Sao.config.limit,
'source': this.update.bind(this),
'displayKey': 'record_name',
'templates': {
'suggestion': function(context) {
var html = jQuery('<div/>');
var img = jQuery('<img/>', {
'class': 'global-search-icon'
});
var img_prm = Sao.common.ICONFACTORY.register_icon(
context.icon);
img_prm.then(function(icon_url) {
img.attr('src', icon_url);
}.bind(this));
html.append(img);
html.append(jQuery('<p/>', {
'class': 'global-search-text'
}).text(context.record_name));
return html;
}
}
});
this.search_entry.on('typeahead:select',
this.match_selected.bind(this));
var completion = new Sao.common.InputCompletion(
this.search_entry,
this.update.bind(this),
this.match_selected.bind(this),
this.format.bind(this));
},
query: function(value) {
if (!arguments.length) {
return this.search_entry.typeahead('val');
} else {
return this.search_entry.typeahead('val', value);
}
format: function(content) {
var el = jQuery('<div/>');
var img = jQuery('<img/>', {
'class': 'global-search-icon'
}).appendTo(el);
Sao.common.ICONFACTORY.register_icon(content.icon).then(
function(icon_url) {
img.attr('src', icon_url);
});
jQuery('<span/>', {
'class': 'global-search-text'
}).text(content.record_name).appendTo(el);
return el;
},
update: function(query, sync, async) {
if (query != this.query()) {
return;
}
update: function(text) {
var ir_model = new Sao.Model('ir.model');
var search_prm = ir_model.execute('global_search',
[query, Sao.config.limit, Sao.main_menu_screen.model_name],
Sao.main_menu_screen.context);
search_prm.then(function(s_results) {
if (query != this.query()) {
return;
}
return ir_model.execute('global_search',
[text, Sao.config.limit, Sao.main_menu_screen.model_name],
Sao.main_menu_screen.context).then(function(s_results) {
var results = [];
for (var i=0, len=s_results.length; i < len; i++) {
results.push({
@@ -404,10 +382,10 @@ var Sao = {};
'icon': s_results[i][5],
});
}
async(results);
return results;
}.bind(this));
},
match_selected: function(event_, item, name) {
match_selected: function(item) {
if (item.model == Sao.main_menu_screen.model_name) {
Sao.Action.exec_keyword('tree_open', {
'model': item.model,
@@ -422,7 +400,7 @@ var Sao = {};
};
Sao.Tab.create(params);
}
this.query('');
this.search_entry.val('');
}
});
+5
View File
@@ -246,3 +246,8 @@ input[type='color'] {
width: 100%;
z-index: 1000;
}
img.global-search-icon {
width: 2em;
height: 2em;
}
+17 -24
View File
@@ -28,7 +28,12 @@
'class': 'form-control',
'placeholder': Sao.i18n.gettext('Search')
});
this.search_list = jQuery('<datalist/>');
this.search_list.uniqueId();
this.search_entry.attr('list', this.search_list.attr('id'));
this.search_entry.keypress(this.key_press.bind(this));
this.search_entry.on('input', this.update.bind(this));
this.but_bookmark = jQuery('<button/>', {
type: 'button',
'class': 'btn btn-default dropdown-toggle',
@@ -78,6 +83,7 @@
'class': 'input-group-btn'
}).append(this.filter_button))
.append(this.search_entry)
.append(this.search_list)
.append(jQuery('<span/>', {
'class': 'input-group-btn'
}).append(this.but_star)
@@ -87,12 +93,6 @@
'class': 'col-md-8'
}).appendTo(this.filter_box));
this.search_entry.attr('autocomplete', 'off');
this.search_entry.typeahead({
'highlight': false
}, {
'source': this.update.bind(this),
});
this.but_prev = jQuery('<button/>', {
type: 'button',
@@ -166,15 +166,15 @@
this.search_entry.val(value);
this.bookmark_match();
},
update: function(query, process) {
var completions = this.screen.domain_parser().completion(query);
var suggestions = [];
update: function() {
var completions = this.screen.domain_parser().completion(
this.get_text());
this.search_list.children().remove();
completions.forEach(function(e) {
suggestions.push({
jQuery('<option/>', {
'value': e.trim()
});
});
process(suggestions);
}).appendTo(this.search_list);
}, this);
},
set_star: function(star) {
var glyphicon = this.but_star.children('span.glyphicon');
@@ -257,18 +257,11 @@
}
this.set_star(false);
},
search_val: function(value) {
if (!arguments.length) {
return this.search_entry.typeahead('val');
} else {
return this.search_entry.typeahead('val', value);
}
},
search_prev: function() {
this.screen.search_prev(this.search_val());
this.screen.search_prev(this.get_text());
},
search_next: function() {
this.screen.search_next(this.search_val());
this.screen.search_next(this.get_text());
},
get_tab_domain: function() {
if (!this.tab) {
@@ -278,7 +271,7 @@
return this.tab_domain[i][1];
},
do_search: function() {
return this.screen.search_filter(this.search_val());
return this.screen.search_filter(this.get_text());
},
key_press: function(e) {
if (e.which == Sao.common.RETURN_KEYCODE) {
@@ -319,7 +312,7 @@
}
},
get_text: function() {
return this.search_val();
return this.search_entry.val();
},
search_box: function() {
var domain_parser = this.screen.domain_parser();
-82
View File
@@ -1,82 +0,0 @@
@import "variables";
.twitter-typeahead {
width: 100%;
.input-group &{
//overwrite `display:inline-block` style
display: table-cell!important;
float: left;
}
& .tt-hint {
background-color: transparent !important;
}
& img.global-search-icon {
width: 2em;
height: 2em;
float: left;
}
& p.global-search-text {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.tt-dataset-actions {
font-style: italic;
font-variant: small-caps;
border-top: 1px #ddd solid;
}
.tt-menu {
//dropdown menu
position: absolute;
top: 100%;
left: 0;
z-index: @zindex-dropdown;
min-width: 160px;
width: 100%;
padding: 5px 0;
margin: 0;
list-style: none;
font-size: @font-size-base;
background-color: @dropdown-bg;
border: 1px solid @dropdown-fallback-border;
border: 1px solid @dropdown-border;
border-bottom-right-radius: @border-radius-base;
border-bottom-left-radius: @border-radius-base;
box-shadow: 0 6px 12px rgba(0,0,0,.175);
background-clip: padding-box;
*border-right-width: 2px;
*border-bottom-width: 2px;
.tt-suggestion {
//item
display: block;
padding: 3px 20px;
clear: both;
font-weight: normal;
line-height: @line-height-base;
color: @dropdown-link-color;
white-space: nowrap;
&.tt-cursor {
//item selected
text-decoration: none;
outline: 0;
background-color: @dropdown-link-hover-bg;
color: @dropdown-link-hover-color;
a {
//link in item selected
color: @dropdown-link-hover-color;
}
}
p {
margin: 0;
}
}
}
+63 -61
View File
@@ -2759,11 +2759,13 @@
})).appendTo(buttons);
this.but_open.click(this.edit.bind(this));
if (!attributes.completion || attributes.completion == "1") {
Sao.common.get_completion(this.entry,
this._update_completion.bind(this),
this._completion_match_selected.bind(this));
Sao.common.get_completion(group,
this._update_completion.bind(this),
this._completion_match_selected.bind(this),
this._completion_action_activated.bind(this));
this.wid_completion = true;
}
this.el.change(this.focus_out.bind(this));
this._readonly = false;
},
get_screen: function() {
@@ -2782,27 +2784,7 @@
if (jQuery.isEmptyObject(value)) {
value = '';
}
this.value(value);
},
// This function is required because once typeahead is activated on an
// entry then .val() function should not be used anymore.
// In order to keep the same semantics as .val() we use the same
// signature
value: function(value) {
if (this.wid_completion) {
if (!arguments.length) {
return this.entry.typeahead('val');
} else {
return this.entry.typeahead('val', value);
}
} else {
if (!arguments.length) {
return this.entry.val();
} else {
return this.entry.val(value);
}
}
this.entry.val(value);
},
get_text: function() {
var record = this.record();
@@ -2811,43 +2793,19 @@
}
return '';
},
_set_completion: function() {
this.entry.data('search', this.read_access());
this.entry.data('create', this.create_access());
},
_update_completion: function(query, sync, async) {
var record = this.record();
if (!record) {
return;
}
var field = this.field();
var value = field.get(record);
if (this.has_target(value)) {
var id = this.id_from_value(value);
if ((id !== undefined) && (id > 0)) {
focus_out: function() {
if (!this.attributes.completion ||
this.attributes.completion == "1") {
if (this.el.find('.dropdown').hasClass('open')) {
return;
}
}
var model = this.get_model();
var prm = Sao.common.update_completion(this.entry, record, field,
model);
prm.done(async);
Sao.View.Form.Many2One._super.focus_out.call(this);
},
_completion_match_selected: function(event_, selected_item, name) {
if (selected_item.id == 'search') {
// it's necessary to set the value so that edit() can get it
// and make the search on the user provided content
this.value(selected_item.query);
this.edit();
this.value('');
} else if (selected_item.id == 'create') {
this.new_();
this.value('');
} else {
this.record().field_set_client(this.field_name,
this.value_from_id(selected_item.id,
selected_item.rec_name), true);
set_value: function(record, field) {
if (field.get_client(record) != this.entry.val()) {
field.set_client(record, this.value_from_id(null, ''));
this.entry.val('');
}
},
display: function(record, field) {
@@ -2863,7 +2821,7 @@
this._set_completion();
if (!record) {
this.value('');
this.entry.val('');
return;
}
this.set_text(field.get_client(record));
@@ -3018,9 +2976,9 @@
} else if (this.has_target(this.record().field_get(
this.field_name)) && editable) {
var value = this.get_text();
if ((value != this.value()) ||
if ((value != this.entry.val()) ||
~delete_keys.indexOf(event_.which)) {
this.value('');
this.entry.val('');
this.record().field_set_client(this.field_name,
this.value_from_id(null, ''));
}
@@ -3036,7 +2994,7 @@
var sao_model = new Sao.Model(model);
if (model && !this.has_target(value)) {
var text = this.value();
var text = this.entry.value();
if (!this._readonly && (text ||
this.field().get_state_attrs(this.record())
.required)) {
@@ -3069,6 +3027,50 @@
});
}
}
},
_set_completion: function() {
var search = this.el.find('.action-search');
if (this.read_access()) {
search.removeClass('disabled');
} else {
search.addClass('disabled');
}
var create = this.el.find('.action-create');
if (this.create_access()) {
create.removeClass('disabled');
} else {
create.addClass('disabled');
}
},
_update_completion: function(text) {
var record = this.record();
if (!record) {
return;
}
var field = this.field();
var value = field.get(record);
if (this.has_target(value)) {
var id = this.id_from_value(value);
if ((id !== undefined) && (id > 0)) {
return jQuery.when();
}
}
var model = this.get_model();
return Sao.common.update_completion(
this.entry, record, field, model);
},
_completion_match_selected: function(value) {
this.record().field_set_client(this.field_name,
this.value_from_id(
value.id, value.rec_name), true);
},
_completion_action_activated: function(action) {
if (action == 'search') {
this.edit();
} else if (action == 'create') {
this.new_();
}
}
});