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 = $(`
- + +
${arr}
diff --git a/package.json b/package.json index e5baaa3..2897c2e 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "karma-module-resolver-preprocessor": "^1.1.3", "karma-qunit": "^4.1.2", "qunit": "^2.19.1", - "rollup": "^2.75.6", + "rollup": "^2.77.2", "rollup-plugin-cleanup": "^3.2.1", "rollup-plugin-terser": "^7.0.2" } diff --git a/src/yafowil/widget/autocomplete/example.py b/src/yafowil/widget/autocomplete/example.py index 8f89d53..0ac6ad0 100644 --- a/src/yafowil/widget/autocomplete/example.py +++ b/src/yafowil/widget/autocomplete/example.py @@ -19,6 +19,9 @@ sunt in culpa qui officia deserunt mollit anim id est laborum.""" lipsum = sorted(set(lipsum.lower().replace('.', '').replace(',', '').split())) +lipsum_dict = {str(idx):word for idx, word in enumerate(lipsum)} +lipsum_list_of_tuples = [(idx, word) for idx, word in enumerate(lipsum)] + def json_response(url): purl = urlparse(url) @@ -37,6 +40,24 @@ def json_data(term): return data +def dict_data(term): + data = lipsum_dict + if term: + result = {str(key):data[key] for key in data if data[key].startswith(term)} + return result + + +def keyval_response(url): + purl = urlparse(url) + qs = parse_qs(purl.query) + data = dict_data(qs.get('term', [''])[0]) + + return { + 'body': json.dumps(data), + 'header': [('Content-Type', 'application/json')] + } + + DOC_STATIC = """\ Autocomplete with static vocabulary ----------------------------------- @@ -99,6 +120,43 @@ def json_response(environ, start_response): return response(environ, start_response) """ +DOC_KEYVAL_LOCAL = """\ +Autocomplete with key/value pairs - local +----------------------------------------- + +The autocomplete source can also be a dict or list of tuples. +Autocomplete happens on the item value. Return value is the item key. + +.. code-block:: python + + lipsum = dict() + lipsum = ([1, 'foo'], [2, 'bar']) + + +.. code-block:: python + + field = factory('#field:autocomplete', props={ + 'label': 'Enter some text (local)', + 'value': '', + 'source': lipsum + }) + +""" + +DOC_KEYVAL_REMOTE = """\ +Autocomplete with key/value pairs - remote +------------------------------------------ + +.. code-block:: python + + field = factory('#field:autocomplete', props={ + 'label': 'Enter some text (remote)', + 'value': '', + 'source': 'yafowil.widget.autocomplete.json', + 'minLength': 1 + }) + +""" def get_example(): static_ac = factory('#field:autocomplete', name='static', props={ @@ -112,8 +170,23 @@ def get_example(): 'source': 'yafowil.widget.autocomplete.json', 'minLength': 1, }) + keyval_local = factory('#field:autocomplete', name='keyval_local', props={ + 'label': 'Enter some text (local)', + 'value': '', + 'source': lipsum_list_of_tuples, + 'dictionary': True, + 'minLength': 1, + }) + keyval_remote = factory('#field:autocomplete', name='keyval_remote', props={ + 'label': 'Enter some text (remote)', + 'value': '', + 'source': 'yafowil.widget.keyval.json', + 'dictionary': True, + 'minLength': 1, + }) routes = { 'yafowil.widget.autocomplete.json': json_response, + 'yafowil.widget.keyval.json': keyval_response, } return [{ 'widget': static_ac, @@ -124,4 +197,13 @@ def get_example(): 'routes': routes, 'doc': DOC_JSON, 'title': 'JSON data autocomplete', + }, { + 'widget': keyval_local, + 'doc': DOC_KEYVAL_LOCAL, + 'title': 'Key/Value autocomplete local', + }, { + 'widget': keyval_remote, + 'routes': routes, + 'doc': DOC_KEYVAL_REMOTE, + 'title': 'Key/Value autocomplete remote', }] diff --git a/src/yafowil/widget/autocomplete/resources/widget.css b/src/yafowil/widget/autocomplete/resources/widget.css index 2b0c665..ac6a41b 100644 --- a/src/yafowil/widget/autocomplete/resources/widget.css +++ b/src/yafowil/widget/autocomplete/resources/widget.css @@ -1,9 +1,9 @@ div.yafowil-widget-autocomplete .hiddenStructure { - display: none; + display: none; } div.autocomplete-dropdown { - display: none; + display: none; position: absolute; z-index: 2000; min-width: 200px; @@ -18,12 +18,12 @@ div.autocomplete-dropdown { } .autocomplete-suggestion { - display: block; + display: block; cursor: default; - padding: 5px 15px; + padding: 5px 15px; } .autocomplete-suggestion.selected { - background: var(--yafowil-accent-color, #0d6efd); + background: var(--yafowil-accent-color, #0d6efd); color: var(--yafowil-accent-font-color, #fff); } \ No newline at end of file diff --git a/src/yafowil/widget/autocomplete/resources/widget.js b/src/yafowil/widget/autocomplete/resources/widget.js index 8616da6..052a2cf 100644 --- a/src/yafowil/widget/autocomplete/resources/widget.js +++ b/src/yafowil/widget/autocomplete/resources/widget.js @@ -2,22 +2,23 @@ var yafowil_autocomplete = (function (exports, $) { 'use strict'; 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()); $(``) - .text(source.substring(0, index)) + .text(this.value.substring(0, index)) .appendTo(this.elem); $(``) - .text(source.substring(index, index + val.length)) + .text(this.value.substring(index, index + term.length)) .appendTo(this.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); this.elem.off('mousedown', this.select).on('mousedown', this.select); @@ -36,7 +37,7 @@ var yafowil_autocomplete = (function (exports, $) { } select() { this.selected = true; - this.widget.select_suggestion(this.value); + this.widget.select_suggestion(this.value, this.key); } } class AutocompleteWidget { @@ -48,9 +49,10 @@ var yafowil_autocomplete = (function (exports, $) { 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'); @@ -118,10 +120,18 @@ var yafowil_autocomplete = (function (exports, $) { 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); @@ -153,13 +163,29 @@ var yafowil_autocomplete = (function (exports, $) { } } autocomplete() { - let val = 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)); + let term = this.input_elem.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, @@ -197,10 +223,9 @@ var yafowil_autocomplete = (function (exports, $) { 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": this.hide_dropdown(); @@ -210,10 +235,10 @@ var yafowil_autocomplete = (function (exports, $) { 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": e.preventDefault(); @@ -251,9 +276,14 @@ var yafowil_autocomplete = (function (exports, $) { break; } } - 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() { for (let suggestion of this.suggestions) { diff --git a/src/yafowil/widget/autocomplete/resources/widget.min.js b/src/yafowil/widget/autocomplete/resources/widget.min.js index f024a8a..72529d1 100644 --- a/src/yafowil/widget/autocomplete/resources/widget.min.js +++ b/src/yafowil/widget/autocomplete/resources/widget.min.js @@ -1 +1 @@ -var yafowil_autocomplete=function(e,t){"use strict";class s{constructor(e,s,i){this.widget=e,this.elem=t("
").addClass("autocomplete-suggestion").appendTo(this.widget.dd_elem);let o=s.toUpperCase().indexOf(i.toUpperCase());t("").text(s.substring(0,o)).appendTo(this.elem),t("").text(s.substring(o,o+i.length)).appendTo(this.elem),t("").text(s.substring(o+i.length)).appendTo(this.elem),this.value=s,this.selected=!1,this.select=this.select.bind(this),this.elem.off("mousedown",this.select).on("mousedown",this.select)}get selected(){return this._selected}set selected(e){e?(this._selected=!0,this.elem.addClass("selected")):(this._selected=!1,this.elem.removeClass("selected"))}select(){this.selected=!0,this.widget.select_suggestion(this.value)}}class i{static initialize(e){t("div.yafowil-widget-autocomplete",e).each((function(){new i(t(this))}))}constructor(e){e.data("yafowil-autocomplete",this),this.elem=e,this.input_elem=t("input.autocomplete",this.elem).attr("spellcheck",!1).attr("autocomplete","off"),this.dd_elem=t("
").addClass("autocomplete-dropdown").appendTo("body"),this.suggestions=[],this.current_focus=0;let s=this.parse_options();this.sourcetype=s.type,this.delay=s.delay,this.min_length=s.minLength,this.parse_source(),this.on_input=this.on_input.bind(this),this.hide_dropdown=this.hide_dropdown.bind(this),this.on_keydown=this.on_keydown.bind(this),this.input_elem.on("focusout",this.hide_dropdown).on("focus input",this.on_input).on("keydown",this.on_keydown),this.autocomplete=this.autocomplete.bind(this)}unload(){clearTimeout(this.timeout),this.input_elem.off("focusout",this.hide_dropdown).off("focus input",this.on_input).off("keydown",this.on_keydown)}parse_options(){let e=t(".autocomplete-params",this.elem).text().split("|"),s=[];for(let t=0;t-1&&l.push(e);s(l)}}else"remote"===this.sourcetype&&(this.source=function(s,i){t.ajax({url:e,data:{term:s.term},dataType:"json",success:function(e){i(e)},error:function(){throw i([]),new Error("Cannot locate JSON at: "+e)}})})}on_input(e){clearTimeout(this.timeout),this.dd_elem.empty().hide(),this.suggestions=[],this.current_focus=-1,this.input_elem.val().length>=this.min_length&&(this.timeout=setTimeout(this.autocomplete,this.delay))}autocomplete(){let e=this.input_elem.val();this.source({term:e},(i=>{if(!i.length)return;for(let t of i)this.suggestions.push(new s(this,t,e));let o,l=t(document).scrollTop(),n=this.elem.offset().top,h=this.elem.offset().left,r=this.elem.outerHeight(),u=this.dd_elem.outerHeight();o=n+r+u>=l+t(window).outerHeight()?n-u:n+r,this.dd_elem.css({top:`${o}px`,left:`${h}px`}),this.dd_elem.show()}))}on_keydown(e){let t=this.dd_elem.scrollTop();switch(e.key){case"ArrowDown":this.current_focus++,this.add_active(!0);break;case"ArrowUp":this.current_focus--,this.add_active(!1);break;case"Enter":if(e.preventDefault(),this.current_focus>-1){let e=this.suggestions[this.current_focus];e.selected=!0,this.input_elem.val(e.value),this.hide_dropdown(),this.input_elem.trigger("blur")}break;case"Escape":this.hide_dropdown(),this.input_elem.trigger("blur");break;case"Tab":if(this.hide_dropdown(),this.current_focus>-1){let e=this.suggestions[this.current_focus];this.input_elem.val(e.value),this.hide_dropdown(),this.input_elem.trigger("blur")}break;case"PageDown":if(e.preventDefault(),this.dd_elem.scrollTop(t+this.dd_elem.height()),this.current_focus>-1){let e=0;for(let t in this.suggestions){this.suggestions[t].elem.offset().top-1){let e=0;for(let t in this.suggestions){this.suggestions[t].elem.offset().top=this.suggestions.length?this.current_focus=0:this.current_focus<0&&(this.current_focus=this.suggestions.length-1);let t=this.suggestions[this.current_focus];t.selected=!0;let s=this.dd_elem.scrollTop(),i=t.elem.offset().top,o=t.elem.outerHeight(),l=this.dd_elem.offset().top,n=this.dd_elem.outerHeight();e?0===this.current_focus?this.dd_elem.scrollTop(0):i+o>l+n&&this.dd_elem.scrollTop(s+o):this.current_focus>=this.suggestions.length-1?this.dd_elem.scrollTop(o*this.suggestions.length):i").addClass("autocomplete-suggestion").appendTo(this.widget.dd_elem),this.key=Array.isArray(s)?s[0]:null,this.value=Array.isArray(s)?s[1]:s;let o=this.value.toUpperCase().indexOf(i.toUpperCase());t("").text(this.value.substring(0,o)).appendTo(this.elem),t("").text(this.value.substring(o,o+i.length)).appendTo(this.elem),t("").text(this.value.substring(o+i.length)).appendTo(this.elem),this.selected=!1,this.select=this.select.bind(this),this.elem.off("mousedown",this.select).on("mousedown",this.select)}get selected(){return this._selected}set selected(e){e?(this._selected=!0,this.elem.addClass("selected")):(this._selected=!1,this.elem.removeClass("selected"))}select(){this.selected=!0,this.widget.select_suggestion(this.value,this.key)}}class i{static initialize(e){t("div.yafowil-widget-autocomplete",e).each((function(){new i(t(this))}))}constructor(e){e.data("yafowil-autocomplete",this),this.elem=e,this.input_elem=t("input.autocomplete-display",this.elem).attr("spellcheck",!1).attr("autocomplete","off"),this.hidden_input=t("input.autocomplete",this.elem),this.dd_elem=t("
").addClass("autocomplete-dropdown").appendTo("body"),this.suggestions=[],this.current_focus=0;let s=this.parse_options();this.sourcetype=s.type,this.delay=s.delay,this.min_length=s.minLength,this.parse_source(),this.on_input=this.on_input.bind(this),this.hide_dropdown=this.hide_dropdown.bind(this),this.on_keydown=this.on_keydown.bind(this),this.input_elem.on("focusout",this.hide_dropdown).on("focus input",this.on_input).on("keydown",this.on_keydown),this.autocomplete=this.autocomplete.bind(this)}unload(){clearTimeout(this.timeout),this.input_elem.off("focusout",this.hide_dropdown).off("focus input",this.on_input).off("keydown",this.on_keydown)}parse_options(){let e=t(".autocomplete-params",this.elem).text().split("|"),s=[];for(let t=0;te.trim())),1===e.length){return void s(i.filter((e=>e.toUpperCase().indexOf(o.toUpperCase())>-1)))}e[1].toUpperCase().indexOf(o.toUpperCase())>-1&&(l[e[0]]=e[1])}s(l)}}else"remote"===this.sourcetype&&(this.source=function(s,i){t.ajax({url:e,data:{term:s.term},dataType:"json",success:function(e){i(e)},error:function(){throw i([]),new Error("Cannot locate JSON at: "+e)}})})}on_input(e){clearTimeout(this.timeout),this.dd_elem.empty().hide(),this.suggestions=[],this.current_focus=-1,this.input_elem.val().length>=this.min_length&&(this.timeout=setTimeout(this.autocomplete,this.delay))}autocomplete(){let e=this.input_elem.val();this.source({term:e},(i=>{if(i instanceof Array){if(!i.length)return;for(let t of i)this.suggestions.push(new s(this,t,e))}else{let t=Object.entries(i);if(0===t.length)return;for(let i of t)this.suggestions.push(new s(this,i,e))}let o,l=t(document).scrollTop(),n=this.elem.offset().top,h=this.elem.offset().left,r=this.elem.outerHeight(),u=this.dd_elem.outerHeight();o=n+r+u>=l+t(window).outerHeight()?n-u:n+r,this.dd_elem.css({top:`${o}px`,left:`${h}px`}),this.dd_elem.show()}))}on_keydown(e){let t=this.dd_elem.scrollTop();switch(e.key){case"ArrowDown":this.current_focus++,this.add_active(!0);break;case"ArrowUp":this.current_focus--,this.add_active(!1);break;case"Enter":if(e.preventDefault(),this.current_focus>-1){let e=this.suggestions[this.current_focus];e.selected=!0,this.select_suggestion(e.value,e.key)}this.input_elem.trigger("blur");break;case"Escape":this.hide_dropdown(),this.input_elem.trigger("blur");break;case"Tab":if(this.hide_dropdown(),this.current_focus>-1){let e=this.suggestions[this.current_focus];e.selected=!0,this.select_suggestion(e.value,e.key)}this.input_elem.trigger("blur");break;case"PageDown":if(e.preventDefault(),this.dd_elem.scrollTop(t+this.dd_elem.height()),this.current_focus>-1){let e=0;for(let t in this.suggestions){this.suggestions[t].elem.offset().top-1){let e=0;for(let t in this.suggestions){this.suggestions[t].elem.offset().top=this.suggestions.length?this.current_focus=0:this.current_focus<0&&(this.current_focus=this.suggestions.length-1);let t=this.suggestions[this.current_focus];t.selected=!0;let s=this.dd_elem.scrollTop(),i=t.elem.offset().top,o=t.elem.outerHeight(),l=this.dd_elem.offset().top,n=this.dd_elem.outerHeight();e?0===this.current_focus?this.dd_elem.scrollTop(0):i+o>l+n&&this.dd_elem.scrollTop(s+o):this.current_focus>=this.suggestions.length-1?this.dd_elem.scrollTop(o*this.suggestions.length):i - + +
http://www.foo.bar/baz
delay,300|minLength,1|type,remote
@@ -51,7 +52,8 @@ def test_source_is_list(self): }) self.checkOutput("""
- + +
foo|bar
delay,300|minLength,1|type,local
@@ -70,7 +72,8 @@ def test_source(widget, data): }) self.checkOutput("""
- + +
http://from.callable/
delay,300|minLength,1|type,remote
""", fxml(widget())) @@ -110,7 +113,8 @@ def test_source(widget, data):
Autocomplete widget is required
+ required="required" type="hidden" value=""/> +
http://from.callable/
delay,300|minLength,1|type,remote
@@ -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 -`_ utilizing -``jquery.ui.autocomplete`` to offer the user a selection based on the input -given so far. +` """ -factory.defaults['autocomplete.type'] = 'text' +factory.defaults['autocomplete.type'] = 'hidden' factory.defaults['autocomplete.class'] = 'autocomplete'