diff --git a/src/plugins/virtual_scroll/plugin.ts b/src/plugins/virtual_scroll/plugin.ts index 3aadfebf..424ff536 100644 --- a/src/plugins/virtual_scroll/plugin.ts +++ b/src/plugins/virtual_scroll/plugin.ts @@ -1,5 +1,5 @@ /** - * Plugin: "restore_on_backspace" (Tom Select) + * Plugin: "virtual_scroll" (Tom Select) * Copyright (c) contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this @@ -151,6 +151,18 @@ export default function(this:TomSelect) { }); + // as the “loading_more” element will be removed from the dropdown, + // we activate the previous option if needed + // to avoid the dropdown being scrolled back to the first one + self.hook('before','refreshOptions',()=>{ + + if (self.activeOption && "option" !== self.activeOption.getAttribute("role")) { + self.setActiveOption(self.activeOption.previousElementSibling as HTMLElement); + } + + }); + + // add templates to dropdown // loading_more if we have another url in the queue // no_more_results if we don't have another url in the queue diff --git a/src/tom-select.ts b/src/tom-select.ts index 697949d3..eea6ea5e 100644 --- a/src/tom-select.ts +++ b/src/tom-select.ts @@ -73,6 +73,7 @@ export default class TomSelect extends MicroPlugin(MicroEvent){ public isFocused : boolean = false; public isInputHidden : boolean = false; public isSetup : boolean = false; + public isDropdownContentStale : boolean = true; public ignoreFocus : boolean = false; public ignoreHover : boolean = false; public hasOptions : boolean = false; @@ -546,8 +547,6 @@ export default class TomSelect extends MicroPlugin(MicroEvent){ self.setupOptions(settings.options,settings.optgroups); self.setValue(settings.items||[],true); // silent prevents recursion - - self.lastQuery = null; // so updated options will be displayed in dropdown } /** @@ -891,7 +890,7 @@ export default class TomSelect extends MicroPlugin(MicroEvent){ } else { value = option.dataset.value; if (typeof value !== 'undefined') { - self.lastQuery = null; + self.isDropdownContentStale = self.settings.hideSelected; self.addItem(value); if (self.settings.closeAfterSelect) { self.close(); @@ -979,7 +978,7 @@ export default class TomSelect extends MicroPlugin(MicroEvent){ loadCallback( options:TomOption[], optgroups:TomOption[] ):void{ const self = this; self.loading = Math.max(self.loading - 1, 0); - self.lastQuery = null; + self.isDropdownContentStale = true; self.clearActiveOption(); // when new results load, focus should be on first option self.setupOptions(options,optgroups); @@ -1356,7 +1355,7 @@ export default class TomSelect extends MicroPlugin(MicroEvent){ } // perform search - if (query !== self.lastQuery) { + if (self.isDropdownContentStale || query !== self.lastQuery) { self.lastQuery = query; result = self.sifter.search(query, Object.assign(options, {score: calculateScore})); self.currentResults = result; @@ -1531,6 +1530,7 @@ export default class TomSelect extends MicroPlugin(MicroEvent){ dropdown_content.innerHTML = ''; append( dropdown_content, html ); + self.isDropdownContentStale = false; // highlight matching terms inline if (self.settings.highlight) { @@ -1649,10 +1649,10 @@ export default class TomSelect extends MicroPlugin(MicroEvent){ return false; } - data.$order = data.$order || ++self.order; - data.$id = self.inputId + '-opt-' + data.$order; - self.options[key] = data; - self.lastQuery = null; + data.$order = data.$order || ++self.order; + data.$id = self.inputId + '-opt-' + data.$order; + self.options[key] = data; + self.isDropdownContentStale = true; if( user_created ){ self.userOptions[key] = user_created; @@ -1793,8 +1793,8 @@ export default class TomSelect extends MicroPlugin(MicroEvent){ replaceNode( item, item_new); } - // invalidate last query because we might have updated the sortField - self.lastQuery = null; + // we might have updated the sortField + self.isDropdownContentStale = true; } /** @@ -1809,7 +1809,7 @@ export default class TomSelect extends MicroPlugin(MicroEvent){ delete self.userOptions[value]; delete self.options[value]; - self.lastQuery = null; + self.isDropdownContentStale = true; self.trigger('option_remove', value); self.removeItem(value, silent); } @@ -1833,7 +1833,7 @@ export default class TomSelect extends MicroPlugin(MicroEvent){ }); this.options = this.sifter.items = selected; - this.lastQuery = null; + this.isDropdownContentStale = true; this.trigger('option_clear'); } @@ -2042,7 +2042,7 @@ export default class TomSelect extends MicroPlugin(MicroEvent){ } self.items.splice(i, 1); - self.lastQuery = null; + self.isDropdownContentStale = true; if (!self.settings.persist && self.userOptions.hasOwnProperty(value)) { self.removeOption(value, silent); } @@ -2128,7 +2128,7 @@ export default class TomSelect extends MicroPlugin(MicroEvent){ */ refreshItems() { var self = this; - self.lastQuery = null; + self.isDropdownContentStale = true; if (self.isSetup) { self.addItems(self.items); diff --git a/test/tests/plugins/virtual_scroll.js b/test/tests/plugins/virtual_scroll.js index 76985010..814dca4f 100644 --- a/test/tests/plugins/virtual_scroll.js +++ b/test/tests/plugins/virtual_scroll.js @@ -27,9 +27,9 @@ describe('plugin: virtual_scroll', function() { it_n('load more data while scrolling',async ()=>{ - var load_calls = 0; + let load_calls = 0; - var test = setup_test('',{ + const test = setup_test('',{ plugins:['virtual_scroll'], labelField: 'value', valueField: 'value', @@ -53,18 +53,21 @@ describe('plugin: virtual_scroll', function() { await asyncType('a'); await waitFor(100); // wait for data to load assert.equal( Object.keys(test.instance.options).length,20,'should load first set of data'); - assert.equal( test.instance.dropdown_content.querySelectorAll('.loading-more-results').length, 1, 'should have loading-more-reuslts template'); + const loadingMoreIndicator = test.instance.dropdown_content.querySelector('.loading-more-results'); + assert.isNotNull( loadingMoreIndicator, 'should have loading_more template'); assert.equal( test.instance.dropdown_content.querySelectorAll('.no-more-results').length, 0 ,'should not have no-more-results template'); assert.equal( test.instance.dropdown_content.querySelectorAll('.option').length, 21 ,'Should display 20 options plus .loading-more-results'); assert.equal( load_calls, 1); + const lastOption = loadingMoreIndicator.previousElementSibling; // load second set of data for "a" - test.instance.scroll(1000,'auto'); // scroll to bottom + test.instance.setActiveOption(loadingMoreIndicator); // scroll to bottom await waitFor(500); // wait for scroll + more data to load assert.equal( Object.keys(test.instance.options).length, 40,'should load second set of data'); - assert.equal( test.instance.dropdown_content.querySelectorAll('.loading-more-results').length, 0, 'should not have loading-more-reuslts template'); + assert.equal( lastOption, test.instance.activeOption, 'previous dataset’s last option should be active') + assert.equal( test.instance.dropdown_content.querySelectorAll('.loading-more-results').length, 0, 'should not have loading_more template'); assert.equal( test.instance.dropdown_content.querySelectorAll('.no-more-results').length, 1 ,'should have no-more-results template'); assert.equal( test.instance.dropdown_content.querySelectorAll('.option').length, 31 ,'Should display 30 options plus .no-more-results'); assert.equal( load_calls, 2); @@ -79,7 +82,7 @@ describe('plugin: virtual_scroll', function() { // load first set of data for "b" await asyncType('\bb'); - await waitFor(100); // wait for data to load + await waitFor(500); // wait for data to load assert.equal( Object.keys(test.instance.options).length,20,'should load new set of data for "b"'); assert.equal( load_calls, 3); });