Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 56 additions & 29 deletions js/src/widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,24 @@ import $ from 'jquery';

export class AutocompleteSuggestion {

constructor(widget, source, val) {
constructor(widget, source, term) {
this.widget = widget;
this.elem = $('<div />')
.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 = $(`<span />`)
.text(source.substring(0, index))
.text(this.value.substring(0, index))
.appendTo(this.elem);
let selected_elem = $(`<strong />`)
.text(source.substring(index, index + val.length))
.text(this.value.substring(index, index + term.length))
.appendTo(this.elem);
let end_elem = $(`<span />`)
.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);
Expand All @@ -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);
}
}

Expand All @@ -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 = $(`<div />`)
.addClass('autocomplete-dropdown')
.appendTo('body');

this.suggestions = [];
this.current_focus = 0;

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand All @@ -207,7 +231,6 @@ export class AutocompleteWidget {
});
this.dd_elem.show();
});

}

on_keydown(e) {
Expand All @@ -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":
Expand All @@ -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":
Expand Down Expand Up @@ -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() {
Expand Down
52 changes: 41 additions & 11 deletions js/tests/test_widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
});
Expand Down Expand Up @@ -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);
});
Expand Down Expand Up @@ -637,7 +666,8 @@ QUnit.module('AutocompleteSuggestion', hooks => {
function create_elem(arr, params) {
let widget_html = $(`
<div class="yafowil-widget-autocomplete">
<input class="autocomplete" type="text" />
<input class="autocomplete-display" type="text" />
<input class="autocomplete" type="hidden" />
<div class="autocomplete-source">
${arr}
</div>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
82 changes: 82 additions & 0 deletions src/yafowil/widget/autocomplete/example.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
-----------------------------------
Expand Down Expand Up @@ -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={
Expand All @@ -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,
Expand All @@ -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',
}]
Loading