diff --git a/js/src/widget.js b/js/src/widget.js index 7ab5518..d59ad5e 100644 --- a/js/src/widget.js +++ b/js/src/widget.js @@ -2,23 +2,24 @@ import $ from 'jquery'; export class AutocompleteSuggestion { - constructor(widget, source, val) { + constructor(widget, source, term) { this.widget = widget; this.elem = $('
') .addClass('autocomplete-suggestion') .appendTo(this.widget.dd_elem); - let index = source.toUpperCase().indexOf(val.toUpperCase()); + this.key = (Array.isArray(source)) ? source[0] : null; + this.value = (Array.isArray(source)) ? source[1] : source; + let index = this.value.toUpperCase().indexOf(term.toUpperCase()); let start_elem = $(``) - .text(source.substring(0, index)) + .text(this.value.substring(0, index)) .appendTo(this.elem); let selected_elem = $(``) - .text(source.substring(index, index + val.length)) + .text(this.value.substring(index, index + term.length)) .appendTo(this.elem); let end_elem = $(``) - .text(source.substring(index + val.length)) + .text(this.value.substring(index + term.length)) .appendTo(this.elem); - this.value = source; this.selected = false; this.select = this.select.bind(this); @@ -41,7 +42,7 @@ export class AutocompleteSuggestion { select() { this.selected = true; - this.widget.select_suggestion(this.value); + this.widget.select_suggestion(this.value, this.key); } } @@ -56,13 +57,13 @@ export class AutocompleteWidget { constructor(elem) { elem.data('yafowil-autocomplete', this); this.elem = elem; - this.input_elem = $('input.autocomplete', this.elem) + this.input_elem = $('input.autocomplete-display', this.elem) .attr('spellcheck', false) .attr('autocomplete', 'off'); + this.hidden_input = $('input.autocomplete', this.elem); this.dd_elem = $(``) .addClass('autocomplete-dropdown') .appendTo('body'); - this.suggestions = []; this.current_focus = 0; @@ -137,11 +138,18 @@ export class AutocompleteWidget { this.source = function(request, response) { let src = source.split('|'), term = request.term, - data = []; - + data = {}; for (let item of src) { - if (item.toUpperCase().indexOf(term.toUpperCase()) > -1) { - data.push(item); + item = item.split(':').map(element => element.trim()); + if (item.length === 1) { + let result = src.filter(word => + word.toUpperCase().indexOf(term.toUpperCase()) > -1 + ) + response(result); + return; + } + if (item[1].toUpperCase().indexOf(term.toUpperCase()) > -1) { + data[item[0]] = item[1]; } } response(data); @@ -176,14 +184,30 @@ export class AutocompleteWidget { } autocomplete() { - let val = this.input_elem.val(); + let term = this.input_elem.val(); - this.source({term: val}, (data) => { - if(!data.length) { - return; - } - for (let item of data) { - this.suggestions.push(new AutocompleteSuggestion(this, item, val)); + this.source({term: term}, (data) => { + if (data instanceof Array) { + if(!data.length) { + return; + } + for (let item of data) { + this.suggestions.push( + new AutocompleteSuggestion(this, item, term) + ); + } + } else { + let entries = Object.entries(data); + if (entries.length === 0) { + return; + } + for (let entry of entries) { + this.suggestions.push(new AutocompleteSuggestion( + this, + entry, + term + )); + } } let scrolltop = $(document).scrollTop(), input_top = this.elem.offset().top, @@ -207,7 +231,6 @@ export class AutocompleteWidget { }); this.dd_elem.show(); }); - } on_keydown(e) { @@ -228,10 +251,9 @@ export class AutocompleteWidget { if (this.current_focus > -1) { let selected_elem = this.suggestions[this.current_focus]; selected_elem.selected = true; - this.input_elem.val(selected_elem.value); - this.hide_dropdown(); - this.input_elem.trigger('blur'); + this.select_suggestion(selected_elem.value, selected_elem.key); } + this.input_elem.trigger('blur'); break; case "Escape": @@ -243,10 +265,10 @@ export class AutocompleteWidget { this.hide_dropdown(); if (this.current_focus > -1) { let selected_elem = this.suggestions[this.current_focus]; - this.input_elem.val(selected_elem.value); - this.hide_dropdown(); - this.input_elem.trigger('blur'); + selected_elem.selected = true; + this.select_suggestion(selected_elem.value, selected_elem.key); } + this.input_elem.trigger('blur'); break; case "PageDown": @@ -289,9 +311,14 @@ export class AutocompleteWidget { } } - select_suggestion(val) { + select_suggestion(value, key) { this.hide_dropdown(); - this.input_elem.val(val); + this.input_elem.val(value); + if (key) { + this.hidden_input.val(key); + } else { + this.hidden_input.val(value); + } } unselect_all() { diff --git a/js/tests/test_widget.js b/js/tests/test_widget.js index d17ab0c..cb6f142 100644 --- a/js/tests/test_widget.js +++ b/js/tests/test_widget.js @@ -23,7 +23,8 @@ QUnit.module('AutocompleteWidget', hooks => { widget = elem.data('yafowil-autocomplete'); assert.deepEqual(widget.elem, elem); - assert.ok(widget.input_elem.is('input.autocomplete')); + assert.ok(widget.input_elem.is('input.autocomplete-display')); + assert.ok(widget.hidden_input.is('input.autocomplete')); assert.ok(widget.dd_elem.is('div.autocomplete-dropdown')); assert.ok(widget.parse_options); @@ -151,7 +152,6 @@ QUnit.module('AutocompleteWidget', hooks => { AutocompleteWidget.initialize(); widget = elem.data('yafowil-autocomplete'); assert.strictEqual(widget.sourcetype, 'remote'); - widget.autocomplete(); let done = assert.async(); @@ -360,13 +360,39 @@ QUnit.module('AutocompleteWidget', hooks => { let done = assert.async(); setTimeout(() => { - let sugs = []; - for (let sug of widget.suggestions) { - sugs.push(sug.value); - } - assert.strictEqual(sugs[0], 'one'); - assert.strictEqual(sugs[1], 'two'); - assert.strictEqual(sugs[2], 'four'); + assert.strictEqual(widget.suggestions[0].value, 'one'); + assert.strictEqual(widget.suggestions[0].key, null); + assert.strictEqual(widget.suggestions[1].value, 'two'); + assert.strictEqual(widget.suggestions[1].key, null); + assert.strictEqual(widget.suggestions[2].value, 'four'); + assert.strictEqual(widget.suggestions[2].key, null); + done(); + }, 10); + }); + + QUnit.test('autocomplete key/value', assert => { + container.empty(); + let arr = "one:eins|two:zwei|three:drei|four:vier"; + elem = create_elem(arr, params); + container.append(elem); + + // initialize widget + AutocompleteWidget.initialize(); + widget = elem.data('yafowil-autocomplete'); + + widget.input_elem.trigger('focus'); + widget.input_elem.val('ei'); + widget.input_elem.trigger('input'); + + let done = assert.async(); + setTimeout(() => { + assert.strictEqual(widget.suggestions.length, 3) + assert.strictEqual(widget.suggestions[0].key, 'one'); + assert.strictEqual(widget.suggestions[0].value, 'eins'); + assert.strictEqual(widget.suggestions[1].key, 'two'); + assert.strictEqual(widget.suggestions[1].value, 'zwei'); + assert.strictEqual(widget.suggestions[2].key, 'three'); + assert.strictEqual(widget.suggestions[2].value, 'drei'); done(); }, 10); }); @@ -463,7 +489,10 @@ QUnit.module('AutocompleteWidget', hooks => { assert.strictEqual(widget.current_focus, 0); // trigger Enter key widget.input_elem.trigger(enter); - assert.strictEqual(widget.input_elem.val(), widget.suggestions[0].value); + assert.strictEqual( + widget.input_elem.val(), + widget.suggestions[0].value + ); done(); }, 10); }); @@ -637,7 +666,8 @@ QUnit.module('AutocompleteSuggestion', hooks => { function create_elem(arr, params) { let widget_html = $(` @@ -51,7 +52,8 @@ def test_source_is_list(self): }) self.checkOutput(""" @@ -70,7 +72,8 @@ def test_source(widget, data): }) self.checkOutput(""" """, fxml(widget())) @@ -110,7 +113,8 @@ def test_source(widget, data): @@ -128,7 +132,7 @@ def test_invalid_source_type(self): widget() self.assertEqual( str(arc.exception), - 'resulting source must be tuple/list or string' + 'resulting source must be tuple/list/dict or string' ) def test_resources(self): diff --git a/src/yafowil/widget/autocomplete/widget.py b/src/yafowil/widget/autocomplete/widget.py index d5924e1..612d3b3 100644 --- a/src/yafowil/widget/autocomplete/widget.py +++ b/src/yafowil/widget/autocomplete/widget.py @@ -1,3 +1,4 @@ +import json from yafowil.base import factory from yafowil.common import generic_extractor from yafowil.common import generic_required_extractor @@ -5,6 +6,7 @@ from yafowil.compat import STR_TYPE from yafowil.utils import attr_value from yafowil.utils import managedprops +from yafowil.utils import cssid @managedprops('source', 'delay', 'minLength') @@ -12,13 +14,26 @@ def autocomplete_renderer(widget, data): result = data.rendered tag = data.tag source = attr_value('source', widget, data) + # XXX: cleanup if isinstance(source, (list, tuple)): - source = '|'.join(source) + try: + source = '|'.join(source) + except: + source = '|'.join(map(lambda x: str(x[0]) + ':' + str(x[1]), source)) + source_type = 'local' + elif isinstance(source, dict): + source = '|'.join('{}:{}'.format(k, v) for k,v in source.items()) source_type = 'local' elif isinstance(source, STR_TYPE): source_type = 'remote' else: - raise ValueError('resulting source must be tuple/list or string') + raise ValueError('resulting source must be tuple/list/dict or string') + + result += tag('input', **{ + 'class': 'autocomplete-display form-control', + 'type': 'text', + 'value': '' + }) result += tag('div', source, **{ 'class': 'autocomplete-source hiddenStructure' }) @@ -52,12 +67,10 @@ def autocomplete_extractor(widget, data): factory.doc['blueprint']['autocomplete'] = """\ Add-on blueprint `yafowil.widget.autocomplete -