describe('AutocompleteEditor', () => { const id = 'testContainer'; const choices = ['yellow', 'red', 'orange', 'green', 'blue', 'gray', 'black', 'white', 'purple', 'lime', 'olive', 'cyan']; beforeEach(function() { this.$container = $(`
`).appendTo('body'); }); afterEach(function() { if (this.$container) { destroy(); this.$container.remove(); } }); describe('open editor', () => { it('should display editor (after hitting ENTER)', () => { handsontable({ columns: [ { editor: 'autocomplete', source: choices } ] }); selectCell(0, 0); expect(isEditorVisible()).toBe(false); keyDownUp('enter'); expect(isEditorVisible()).toBe(true); }); it('should display editor (after hitting F2)', () => { handsontable({ columns: [ { editor: 'autocomplete', source: choices } ] }); selectCell(0, 0); expect(isEditorVisible()).toBe(false); keyDownUp('f2'); expect(isEditorVisible()).toBe(true); }); it('should display editor (after doubleclicking)', () => { handsontable({ columns: [ { editor: 'autocomplete', source: choices } ] }); selectCell(0, 0); expect(isEditorVisible()).toBe(false); mouseDoubleClick($(getCell(0, 0))); expect(isEditorVisible()).toBe(true); }); // see https://github.com/handsontable/handsontable/issues/3380 it('should not throw error while selecting the next cell by hitting enter key', () => { const spy = jasmine.createSpyObj('error', ['test']); const prevError = window.onerror; window.onerror = function() { spy.test(); }; handsontable({ columns: [{ editor: 'autocomplete', source: choices }] }); selectCell(0, 0); keyDownUp('enter'); keyDownUp('enter'); keyDownUp('enter'); expect(spy.test.calls.count()).toBe(0); window.onerror = prevError; }); }); describe('choices', () => { it('should display given choices (array)', (done) => { handsontable({ columns: [ { editor: 'autocomplete', source: choices } ] }); selectCell(0, 0); const editor = $('.autocompleteEditor'); keyDownUp('enter'); setTimeout(() => { expect(editor.find('tbody td:eq(0)').text()).toEqual(choices[0]); expect(editor.find('tbody td:eq(1)').text()).toEqual(choices[1]); expect(editor.find('tbody td:eq(2)').text()).toEqual(choices[2]); expect(editor.find('tbody td:eq(3)').text()).toEqual(choices[3]); expect(editor.find('tbody td:eq(4)').text()).toEqual(choices[4]); done(); }, 100); }); it('should call source function with context set as cellProperties', (done) => { const source = jasmine.createSpy('source'); let context; source.and.callFake(function(query, process) { process(choices); context = this; }); const hot = handsontable({ columns: [ { editor: 'autocomplete', source } ] }); selectCell(0, 0); source.calls.reset(); keyDownUp('enter'); setTimeout(() => { expect(context.instance).toBe(hot); expect(context.row).toBe(0); expect(context.col).toBe(0); done(); }, 200); }); it('should display given choices (sync function)', (done) => { const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(choices); }); handsontable({ columns: [ { editor: 'autocomplete', source: syncSources } ] }); selectCell(0, 0); const editor = $('.autocompleteEditor'); syncSources.calls.reset(); keyDownUp('enter'); setTimeout(() => { expect(editor.find('tbody td:eq(0)').text()).toEqual(choices[0]); expect(editor.find('tbody td:eq(1)').text()).toEqual(choices[1]); expect(editor.find('tbody td:eq(2)').text()).toEqual(choices[2]); expect(editor.find('tbody td:eq(3)').text()).toEqual(choices[3]); expect(editor.find('tbody td:eq(4)').text()).toEqual(choices[4]); done(); }, 200); }); it('should display given choices (async function)', (done) => { const asyncSources = jasmine.createSpy('asyncSources'); asyncSources.and.callFake((process) => { process(choices); }); handsontable({ columns: [ { editor: 'autocomplete', source(query, process) { setTimeout(() => { asyncSources(process); }, 0); } } ] }); selectCell(0, 0); const editor = $('.autocompleteEditor'); keyDownUp('enter'); setTimeout(() => { expect(asyncSources.calls.count()).toEqual(1); expect(editor.find('tbody td:eq(0)').text()).toEqual(choices[0]); expect(editor.find('tbody td:eq(1)').text()).toEqual(choices[1]); expect(editor.find('tbody td:eq(2)').text()).toEqual(choices[2]); expect(editor.find('tbody td:eq(3)').text()).toEqual(choices[3]); expect(editor.find('tbody td:eq(4)').text()).toEqual(choices[4]); done(); }, 200); }); it('should NOT update choices list, after cursor leaves and enters the list (#1330)', (done) => { spyOn(Handsontable.editors.AutocompleteEditor.prototype, 'updateChoicesList').and.callThrough(); const updateChoicesList = Handsontable.editors.AutocompleteEditor.prototype.updateChoicesList; const hot = handsontable({ columns: [ { editor: 'autocomplete', source: choices } ] }); selectCell(0, 0); const editor = hot.getActiveEditor(); keyDownUp('enter'); setTimeout(() => { updateChoicesList.calls.reset(); $(editor.htContainer).find('.htCore tr:eq(0) td:eq(0)').mouseenter(); $(editor.htContainer).find('.htCore tr:eq(0) td:eq(0)').mouseleave(); $(editor.htContainer).find('.htCore tr:eq(0) td:eq(0)').mouseenter(); }, 200); setTimeout(() => { expect(updateChoicesList).not.toHaveBeenCalled(); done(); }, 300); }); it('should update choices list exactly once after a key is pressed (#1330)', (done) => { spyOn(Handsontable.editors.AutocompleteEditor.prototype, 'updateChoicesList').and.callThrough(); const updateChoicesList = Handsontable.editors.AutocompleteEditor.prototype.updateChoicesList; const hot = handsontable({ columns: [ { editor: 'autocomplete', source: choices } ] }); selectCell(0, 0); const editor = hot.getActiveEditor(); updateChoicesList.calls.reset(); keyDownUp('enter'); setTimeout(() => { updateChoicesList.calls.reset(); editor.TEXTAREA.value = 'red'; $(editor.TEXTAREA).simulate('keydown', { keyCode: 'd'.charCodeAt(0) }); }, 200); setTimeout(() => { expect(updateChoicesList.calls.count()).toEqual(1); done(); }, 100); }); it('should not initialize the dropdown with unneeded scrollbars (scrollbar causing a scrollbar issue)', (done) => { spyOn(Handsontable.editors.AutocompleteEditor.prototype, 'updateChoicesList').and.callThrough(); const updateChoicesList = Handsontable.editors.AutocompleteEditor.prototype.updateChoicesList; const hot = handsontable({ data: [ [ 'blue' ], [], [], [] ], columns: [ { editor: 'autocomplete', source: choices } ] }); selectCell(0, 0); const editor = hot.getActiveEditor(); updateChoicesList.calls.reset(); keyDownUp('enter'); setTimeout(() => { expect(editor.htContainer.scrollWidth).toEqual(editor.htContainer.clientWidth); done(); }, 200); }); it('autocomplete list should have textarea dimensions', (done) => { const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(choices); }); handsontable({ colWidths: [200], columns: [ { editor: 'autocomplete', source: syncSources } ] }); selectCell(0, 0); const editor = $('.handsontableInputHolder'); syncSources.calls.reset(); keyDownUp('enter'); setTimeout(() => { // -2 for transparent borders expect(editor.find('.autocompleteEditor .htCore td').width()).toEqual(editor.find('.handsontableInput').width() - 2); expect(editor.find('.autocompleteEditor .htCore td').width()).toBeGreaterThan(187); done(); }, 200); }); it('autocomplete list should have the suggestion table dimensions, when trimDropdown option is set to false', (done) => { const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(['long text', 'even longer text', 'extremely long text in the suggestion list', 'short text', 'text', 'another', 'yellow', 'black']); }); handsontable({ colWidths: [200], columns: [ { editor: 'autocomplete', source: syncSources } ], trimDropdown: false, }); selectCell(0, 0); const editor = $('.handsontableInputHolder'); syncSources.calls.reset(); keyDownUp('enter'); setTimeout(() => { expect(editor.find('.autocompleteEditor .htCore td').eq(0).width()).toBeGreaterThan(editor.find('.handsontableInput').width()); done(); }, 200); }); // TODO: This test never properly tests the case of refreshing editor after re-render the table. Previously this // test passes because sleep timeout was small enough to read the valid width before the editor element was resized. // Related issue #5103 xit('autocomplete textarea should have cell dimensions (after render)', async() => { const data = [ ['a', 'b'], ['c', 'd'] ]; handsontable({ data, minRows: 4, minCols: 4, minSpareRows: 4, minSpareCols: 4, cells() { return { type: Handsontable.AutocompleteCell }; } }); selectCell(1, 1); keyDownUp('enter'); await sleep(10); data[1][1] = 'dddddddddddddddddddd'; render(); await sleep(10); const $td = spec().$container.find('.htCore tbody tr:eq(1) td:eq(1)'); expect(autocompleteEditor().width()).toEqual($td.width()); }); it('should invoke beginEditing only once after doubleclicking on a cell (#1011)', () => { const hot = handsontable({ columns: [ {}, {}, { type: 'autocomplete', source: choices } ] }); selectCell(0, 2); spyOn(hot.getActiveEditor(), 'beginEditing'); expect(hot.getActiveEditor().beginEditing.calls.count()).toBe(0); mouseDoubleClick(getCell(0, 2)); expect(hot.getActiveEditor().beginEditing.calls.count()).toBe(1); mouseDoubleClick(getCell(1, 2)); expect(hot.getActiveEditor().beginEditing.calls.count()).toBe(2); mouseDoubleClick(getCell(2, 2)); expect(hot.getActiveEditor().beginEditing.calls.count()).toBe(3); }); it('should not display all the choices from a long source list and not leave any unused space in the dropdown', async() => { handsontable({ columns: [ { type: 'autocomplete', source: [ 'Acura', 'Audi', 'BMW', 'Buick', 'Cadillac', 'Chevrolet', 'Chrysler', 'Citroen', 'Dodge', 'Eagle', 'Ferrari', 'Ford', 'General Motors', 'GMC', 'Honda', 'Hummer', 'Hyundai', 'Infiniti', 'Isuzu', 'Jaguar', 'Jeep', 'Kia', 'Lamborghini', 'Land Rover', 'Lexus', 'Lincoln', 'Lotus', 'Mazda', 'Mercedes-Benz', 'Mercury', 'Mitsubishi', 'Nissan', 'Oldsmobile', 'Peugeot', 'Pontiac', 'Porsche', 'Regal', 'Renault', 'Saab', 'Saturn', 'Seat', 'Skoda', 'Subaru', 'Suzuki', 'Toyota', 'Volkswagen', 'Volvo'] } ] }); selectCell(0, 0); keyDownUp('enter'); const $autocomplete = autocomplete(); const $autocompleteHolder = $autocomplete.find('.ht_master .wtHolder').first(); await sleep(100); expect($autocomplete.find('td').first().text()).toEqual('Acura'); $autocompleteHolder.scrollTop($autocompleteHolder[0].scrollHeight); await sleep(100); expect($autocomplete.find('td').last().text()).toEqual('Volvo'); }); it('should display the choices, regardless if they\'re declared as string or numeric', (done) => { handsontable({ columns: [ { editor: 'autocomplete', source: ['1', '2', 3, '4', 5, 6] } ] }); selectCell(0, 0); const editor = $('.autocompleteEditor'); keyDownUp('enter'); setTimeout(() => { expect(editor.find('tbody td:eq(0)').text()).toEqual('1'); expect(editor.find('tbody td:eq(1)').text()).toEqual('2'); expect(editor.find('tbody td:eq(2)').text()).toEqual('3'); expect(editor.find('tbody td:eq(3)').text()).toEqual('4'); expect(editor.find('tbody td:eq(4)').text()).toEqual('5'); expect(editor.find('tbody td:eq(5)').text()).toEqual('6'); done(); }, 100); }); it('should display the choices, regardless if they\'re declared as string or numeric, when data is present', (done) => { handsontable({ data: Handsontable.helper.createSpreadsheetData(10, 1), columns: [ { editor: 'autocomplete', source: ['1', '2', 3, '4', 5, 6] } ] }); selectCell(0, 0); keyDownUp('backspace'); const editor = $('.autocompleteEditor'); keyDownUp('enter'); setTimeout(() => { expect(editor.find('tbody td:eq(0)').text()).toEqual('1'); expect(editor.find('tbody td:eq(1)').text()).toEqual('2'); expect(editor.find('tbody td:eq(2)').text()).toEqual('3'); expect(editor.find('tbody td:eq(3)').text()).toEqual('4'); expect(editor.find('tbody td:eq(4)').text()).toEqual('5'); expect(editor.find('tbody td:eq(5)').text()).toEqual('6'); done(); }, 100); }); it('should display the dropdown above the editor, when there is not enough space below the cell AND there is more space above the cell', (done) => { handsontable({ data: Handsontable.helper.createSpreadsheetData(30, 30), columns: [ { editor: 'autocomplete', source: choices }, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {} ], width: 400, height: 400 }); setDataAtCell(29, 0, ''); selectCell(29, 0); mouseDoubleClick($(getCell(29, 0))); setTimeout(() => { const autocompleteEditor = $('.autocompleteEditor'); expect(autocompleteEditor.css('position')).toEqual('absolute'); expect(autocompleteEditor.css('top')).toEqual(`${(-1) * autocompleteEditor.height()}px`); done(); }, 200); }); it('should flip the dropdown upwards when there is no more room left below the cell after filtering the choice list', async() => { const hot = handsontable({ data: Handsontable.helper.createSpreadsheetData(30, 30), columns: [ { editor: 'autocomplete', source: choices }, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {} ], width: 400, height: 400 }); setDataAtCell(26, 0, 'b'); selectCell(26, 0); hot.view.wt.wtTable.holder.scrollTop = 999; mouseDoubleClick($(getCell(26, 0))); const autocompleteEditor = $('.autocompleteEditor'); await sleep(100); expect(autocompleteEditor.css('position')).toEqual('relative'); autocompleteEditor.siblings('textarea').first().val(''); keyDownUp('backspace'); await sleep(100); expect(autocompleteEditor.css('position')).toEqual('absolute'); expect(autocompleteEditor.css('top')).toEqual(`${(-1) * autocompleteEditor.height()}px`); }); }); describe('closing editor', () => { it('should destroy editor when value change with mouse click on suggestion', (done) => { const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(choices); }); handsontable({ columns: [ { editor: 'autocomplete', source: syncSources } ] }); selectCell(0, 0); keyDownUp('enter'); setTimeout(() => { autocomplete().find('tbody td:eq(3)').simulate('mousedown'); expect(getDataAtCell(0, 0)).toEqual('green'); done(); }, 200); }); it('should not change value type from `numeric` to `string` after mouse click suggestion - ' + 'test no. 1 #4143', (done) => { handsontable({ columns: [ { editor: 'autocomplete', source: [1, 2, 3, 4, 5, 11, 14] } ] }); selectCell(0, 0); keyDownUp('enter'); setTimeout(() => { autocomplete().find('tbody td:eq(0)').simulate('mousedown'); expect(typeof getDataAtCell(0, 0)).toEqual('number'); done(); }, 200); }); it('should not change value type from `numeric` to `string` after mouse click on suggestion - ' + 'test no. 2 #4143', (done) => { const syncSources = jasmine.createSpy('syncSources'); const source = [1, 2, 3, 4, 5, 11, 14]; syncSources.and.callFake((query, process) => { process(source); }); handsontable({ columns: [ { editor: 'autocomplete', source: syncSources } ] }); selectCell(0, 0); keyDownUp('enter'); setTimeout(() => { autocomplete().find('tbody td:eq(0)').simulate('mousedown'); expect(typeof getDataAtCell(0, 0)).toEqual('number'); done(); }, 200); }); it('should call `afterChange` hook with proper value types - test no. 1 #4143', (done) => { let changesInside; const afterChange = (changes, source) => { if (source !== 'loadData') { changesInside = changes; } }; handsontable({ columns: [ { editor: 'autocomplete', source: [1, 2, 3, 4, 5, 11, 14] } ], afterChange }); selectCell(0, 0); keyDownUp('enter'); setTimeout(() => { autocomplete().find('tbody td:eq(1)').simulate('mousedown'); expect(changesInside[0]).toEqual([0, 0, null, 2]); done(); }, 200); }); it('should call `afterChange` hook with proper value types - test no. 2 #4143', (done) => { let changesInside; const afterChange = (changes, source) => { if (source !== 'loadData') { changesInside = changes; } }; const syncSources = jasmine.createSpy('syncSources'); const source = [1, 2, 3, 4, 5, 11, 14]; syncSources.and.callFake((query, process) => { process(source); }); handsontable({ columns: [ { editor: 'autocomplete', source: syncSources } ], afterChange }); selectCell(0, 0); keyDownUp('enter'); setTimeout(() => { autocomplete().find('tbody td:eq(1)').simulate('mousedown'); expect(changesInside[0]).toEqual([0, 0, null, 2]); done(); }, 200); }); it('should not change value type from `numeric` to `string` when written down value from set of suggestions #4143', (done) => { const syncSources = jasmine.createSpy('syncSources'); const source = [1, 2, 3, 4, 5, 11, 14]; syncSources.and.callFake((query, process) => { process(source); }); handsontable({ columns: [ { editor: 'autocomplete', source: syncSources } ] }); selectCell(0, 0); keyDownUp('enter'); keyDownUp('backspace'); document.activeElement.value = '1'; $(document.activeElement).simulate('keyup'); setTimeout(() => { keyDownUp('enter'); expect(getDataAtCell(0, 0)).toEqual(1); done(); }, 200); }); it('should destroy editor when value change with Enter on suggestion', (done) => { const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(choices); }); handsontable({ columns: [ { editor: 'autocomplete', source: syncSources } ] }); selectCell(0, 0); keyDownUp('enter'); setTimeout(() => { keyDownUp('arrow_down'); keyDownUp('arrow_down'); keyDownUp('arrow_down'); keyDownUp('arrow_down'); keyDownUp('enter'); expect(getDataAtCell(0, 0)).toEqual('green'); done(); }, 200); }); it('should destroy editor when pressed Enter then Esc', async() => { const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(choices); }); handsontable({ columns: [ { editor: 'autocomplete', source: syncSources } ] }); selectCell(0, 0); keyDownUp('enter'); await sleep(200); expect(isEditorVisible(autocompleteEditor())).toBe(true); keyDownUp('esc'); expect(isEditorVisible(autocompleteEditor())).toBe(false); }); it('should destroy editor when mouse double clicked then Esc', async() => { const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(choices); }); handsontable({ columns: [ { editor: 'autocomplete', source: syncSources } ] }); selectCell(0, 0); mouseDoubleClick(getCell(0, 0)); await sleep(200); expect(isEditorVisible(autocompleteEditor())).toBe(true); keyDownUp('esc'); expect(isEditorVisible(autocompleteEditor())).toBe(false); }); it('cancel editing (Esc) should restore the previous value', (done) => { const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(choices); }); handsontable({ columns: [ { editor: 'autocomplete', source: syncSources } ] }); setDataAtCell(0, 0, 'black'); selectCell(0, 0); keyDownUp('enter'); setTimeout(() => { autocomplete().siblings('.handsontableInput').val('ye'); keyDownUp(69); // e keyDownUp('esc'); expect(getDataAtCell(0, 0)).toEqual('black'); done(); }, 200); }); it('should destroy editor when clicked outside the table', async() => { const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(choices); }); handsontable({ columns: [ { editor: 'autocomplete', source: syncSources } ] }); selectCell(0, 0); mouseDoubleClick(getCell(0, 0)); await sleep(200); expect(isEditorVisible(autocompleteEditor())).toBe(true); $('body').simulate('mousedown'); expect(isEditorVisible(autocompleteEditor())).toBe(false); }); it('should show fillHandle element again after close editor', (done) => { const syncSources = jasmine.createSpy('syncSources'); syncSources.plan = function(query, process) { process(choices.filter(choice => choice.indexOf(query) !== -1)); }; handsontable({ columns: [ { type: 'autocomplete', source: syncSources, strict: false }, {} ] }); selectCell(1, 0); keyDownUp('x'); // Trigger quick edit mode keyDownUp('enter'); setTimeout(() => { expect($('#testContainer.handsontable > .handsontable .wtBorder.current.corner:visible').length).toEqual(1); done(); }, 200); }); }); describe('non strict mode', () => { it('should allow any value in non strict mode (close editor with ENTER)', (done) => { const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(choices); }); handsontable({ columns: [ { editor: 'autocomplete', source: syncSources } ] }); selectCell(0, 0); keyDownUp('enter'); setTimeout(() => { const editor = $('.handsontableInput'); editor.val('foo'); keyDownUp('enter'); expect(getDataAtCell(0, 0)).toEqual('foo'); done(); }, 200); }); it('should allow any value in non strict mode (close editor by clicking on table)', (done) => { const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(choices); }); handsontable({ columns: [ { editor: 'autocomplete', source: syncSources } ] }); selectCell(0, 0); keyDownUp('enter'); setTimeout(() => { const editor = $('.handsontableInput'); editor.val('foo'); spec().$container.find('tbody tr:eq(1) td:eq(0)').simulate('mousedown'); expect(getDataAtCell(0, 0)).toEqual('foo'); done(); }, 200); }); it('should save the value from textarea after hitting ENTER', (done) => { const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(choices.filter(choice => choice.indexOf(query) !== -1)); }); const hot = handsontable({ columns: [ { editor: 'autocomplete', source: syncSources } ] }); selectCell(0, 0); const editorInput = $('.handsontableInput'); expect(getDataAtCell(0, 0)).toBeNull(); keyDownUp('enter'); setTimeout(() => { syncSources.calls.reset(); editorInput.val('b'); keyDownUp('b'.charCodeAt(0)); }, 200); setTimeout(() => { const ac = hot.getActiveEditor(); const innerHot = ac.htEditor; expect(innerHot.getData()).toEqual([ ['blue'], ['black'] ]); const selected = innerHot.getSelected(); expect(selected).toBeUndefined(); keyDownUp('enter'); expect(getDataAtCell(0, 0)).toEqual('b'); done(); }, 400); }); }); describe('strict mode', () => { it('strict mode should NOT use value if it DOES NOT match the list (sync reponse is empty)', (done) => { const onAfterValidate = jasmine.createSpy('onAfterValidate'); const onAfterChange = jasmine.createSpy('onAfterChange'); const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process([]); // hardcoded empty result }); handsontable({ data: [ ['one', 'two'], ['three', 'four'] ], columns: [ { type: 'autocomplete', source: syncSources, allowInvalid: false, strict: true }, {} ], afterValidate: onAfterValidate, afterChange: onAfterChange }); setDataAtCell(0, 0, 'unexistent'); setTimeout(() => { expect(getData()).toEqual([ ['one', 'two'], ['three', 'four'] ]); expect(syncSources.calls.count()).toEqual(1); expect(onAfterValidate.calls.count()).toEqual(1); expect(onAfterChange.calls.count()).toEqual(1); // 1 for loadData (it is not called after failed edit) done(); }, 200); }); it('strict mode should use value if it DOES match the list (sync reponse is not empty)', (done) => { const onAfterValidate = jasmine.createSpy('onAfterValidate'); const onAfterChange = jasmine.createSpy('onAfterChange'); const syncSources = jasmine.createSpy('asyncSources'); syncSources.and.callFake((query, process) => { process(choices); // hardcoded empty result }); handsontable({ data: [ ['one', 'two'], ['three', 'four'] ], columns: [ { type: 'autocomplete', source: syncSources, allowInvalid: false, strict: true }, {} ], afterValidate: onAfterValidate, afterChange: onAfterChange }); setDataAtCell(0, 0, 'yellow'); setTimeout(() => { expect(getData()).toEqual([ ['yellow', 'two'], ['three', 'four'] ]); expect(syncSources.calls.count()).toEqual(1); expect(onAfterValidate.calls.count()).toEqual(1); expect(onAfterChange.calls.count()).toEqual(2); // 1 for loadData and 1 for setDataAtCell done(); }, 200); }); it('strict mode should NOT use value if it DOES NOT match the list (async reponse is empty)', (done) => { const onAfterValidate = jasmine.createSpy('onAfterValidate'); const onAfterChange = jasmine.createSpy('onAfterChange'); const asyncSources = jasmine.createSpy('asyncSources'); asyncSources.and.callFake((query, process) => { setTimeout(() => { process([]); // hardcoded empty result }); }); handsontable({ data: [ ['one', 'two'], ['three', 'four'] ], columns: [ { type: 'autocomplete', source: asyncSources, allowInvalid: false, strict: true }, {} ], afterValidate: onAfterValidate, afterChange: onAfterChange }); setDataAtCell(0, 0, 'unexistent'); setTimeout(() => { expect(getData()).toEqual([ ['one', 'two'], ['three', 'four'] ]); expect(asyncSources.calls.count()).toEqual(1); expect(onAfterValidate.calls.count()).toEqual(1); expect(onAfterChange.calls.count()).toEqual(1); // 1 for loadData (it is not called after failed edit) done(); }, 200); }); it('strict mode should use value if it DOES match the list (async reponse is not empty)', (done) => { const onAfterValidate = jasmine.createSpy('onAfterValidate'); const onAfterChange = jasmine.createSpy('onAfterChange'); const asyncSources = jasmine.createSpy('asyncSources'); asyncSources.and.callFake((query, process) => { setTimeout(() => { process(choices); // hardcoded empty result }); }); handsontable({ data: [ ['one', 'two'], ['three', 'four'] ], columns: [ { type: 'autocomplete', source: asyncSources, allowInvalid: false, strict: true }, {} ], afterValidate: onAfterValidate, afterChange: onAfterChange }); setDataAtCell(0, 0, 'yellow'); setTimeout(() => { expect(getData()).toEqual([ ['yellow', 'two'], ['three', 'four'] ]); expect(asyncSources.calls.count()).toEqual(1); expect(onAfterValidate.calls.count()).toEqual(1); expect(onAfterChange.calls.count()).toEqual(2); // 1 for loadData and 1 for setDataAtCell done(); }, 200); }); it('strict mode mark value as invalid if it DOES NOT match the list (sync reponse is empty)', (done) => { const onAfterValidate = jasmine.createSpy('onAfterValidate'); const onAfterChange = jasmine.createSpy('onAfterChange'); const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process([]); // hardcoded empty result }); handsontable({ data: [ ['one', 'two'], ['three', 'four'] ], columns: [ { type: 'autocomplete', source: syncSources, allowInvalid: true, strict: true }, {} ], afterValidate: onAfterValidate, afterChange: onAfterChange }); expect(getCellMeta(0, 0).valid).not.toBe(false); expect($(getCell(0, 0)).hasClass('htInvalid')).toBe(false); setDataAtCell(0, 0, 'unexistent'); setTimeout(() => { expect(getData()).toEqual([ ['unexistent', 'two'], ['three', 'four'] ]); expect(getCellMeta(0, 0).valid).toBe(false); expect($(getCell(0, 0)).hasClass('htInvalid')).toBe(true); done(); }, 200); }); it('should select the best matching option after hitting ENTER', (done) => { const onAfterValidate = jasmine.createSpy('onAfterValidate'); const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(choices.filter(choice => choice.indexOf(query) !== -1)); }); const hot = handsontable({ columns: [ { editor: 'autocomplete', source: syncSources, strict: true } ], afterValidate: onAfterValidate }); selectCell(0, 0); const editorInput = $('.handsontableInput'); expect(getDataAtCell(0, 0)).toBeNull(); keyDownUp('enter'); setTimeout(() => { syncSources.calls.reset(); editorInput.val('b'); keyDownUp('b'.charCodeAt(0)); }, 200); setTimeout(() => { const ac = hot.getActiveEditor(); const innerHot = ac.htEditor; expect(innerHot.getData()).toEqual([ ['blue'], ['black'] ]); const selected = innerHot.getSelected()[0]; const selectedData = innerHot.getDataAtCell(selected[0], selected[1]); expect(selectedData).toEqual('blue'); onAfterValidate.calls.reset(); keyDownUp('enter'); }, 400); setTimeout(() => { expect(getDataAtCell(0, 0)).toEqual('blue'); done(); }, 600); }); it('should select the best matching option after hitting TAB', (done) => { const onAfterValidate = jasmine.createSpy('onAfterValidate'); const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(choices.filter(choice => choice.indexOf(query) !== -1)); }); const hot = handsontable({ columns: [ { editor: 'autocomplete', source: syncSources, strict: true } ], afterValidate: onAfterValidate }); selectCell(0, 0); const editorInput = $('.handsontableInput'); expect(getDataAtCell(0, 0)).toBeNull(); keyDownUp('enter'); setTimeout(() => { syncSources.calls.reset(); editorInput.val('b'); keyDownUp('b'.charCodeAt(0)); }, 200); setTimeout(() => { const ac = hot.getActiveEditor(); const innerHot = ac.htEditor; expect(innerHot.getData()).toEqual([ ['blue'], ['black'] ]); const selected = innerHot.getSelected()[0]; const selectedData = innerHot.getDataAtCell(selected[0], selected[1]); expect(selectedData).toEqual('blue'); onAfterValidate.calls.reset(); keyDownUp('tab'); }, 400); setTimeout(() => { expect(getDataAtCell(0, 0)).toEqual('blue'); done(); }, 600); }); it('should mark list item corresponding to current cell value as selected', (done) => { const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(['red', 'dark-yellow', 'yellow', 'light-yellow', 'black']); }); handsontable({ columns: [ { editor: 'autocomplete', source: syncSources, strict: true } ], data: [ ['yellow'], ['red'], ['blue'] ] }); selectCell(0, 0); keyDownUp('enter'); setTimeout(() => { expect(autocomplete().find('.current').text()).toEqual(getDataAtCell(0, 0)); done(); }, 200); }); }); describe('filtering', () => { it('typing in textarea should filter the lookup list', (done) => { const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(choices.filter(choice => choice.indexOf(query) !== -1)); }); const hot = handsontable({ columns: [ { editor: 'autocomplete', source: syncSources } ] }); selectCell(0, 0); const editorInput = $('.handsontableInput'); expect(getDataAtCell(0, 0)).toBeNull(); keyDownUp('enter'); setTimeout(() => { syncSources.calls.reset(); editorInput.val('e'); keyDownUp(69); // e }, 200); setTimeout(() => { const ac = hot.getActiveEditor(); const innerHot = ac.htEditor; expect(innerHot.getData()).toEqual([ ['red'], ['yellow'], ['green'], ['blue'], ['lime'], ['white'], ['olive'], ['orange'], ['purple'] ]); syncSources.calls.reset(); editorInput.val('ed'); keyDownUp(68); // d }, 400); setTimeout(() => { const ac = hot.getActiveEditor(); const innerHot = ac.htEditor; expect(innerHot.getData()).toEqual([ ['red'] ]); done(); }, 600); }); it('default filtering should be case insensitive', (done) => { const hot = handsontable({ columns: [ { editor: 'autocomplete', source: choices } ] }); selectCell(0, 0); const editorInput = $('.handsontableInput'); expect(getDataAtCell(0, 0)).toBeNull(); keyDownUp('enter'); editorInput.val('e'); keyDownUp(69); // e setTimeout(() => { const ac = hot.getActiveEditor(); const innerHot = ac.htEditor; expect(innerHot.getData()).toEqual([ ['red'], ['yellow'], ['green'], ['blue'], ['lime'], ['white'], ['olive'], ['orange'], ['purple'] ]); editorInput.val('e'); keyDownUp(69); // E (same as 'e') }, 50); setTimeout(() => { const ac = hot.getActiveEditor(); const innerHot = ac.htEditor; expect(innerHot.getData()).toEqual([ ['red'], ['yellow'], ['green'], ['blue'], ['lime'], ['white'], ['olive'], ['orange'], ['purple'] ]); done(); }, 100); }); it('default filtering should be case sensitive when filteringCaseSensitive is false', (done) => { const hot = handsontable({ columns: [ { editor: 'autocomplete', source: choices, filteringCaseSensitive: true } ] }); selectCell(0, 0); const editorInput = $('.handsontableInput'); expect(getDataAtCell(0, 0)).toBeNull(); keyDownUp('enter'); editorInput.val('e'); keyDownUp(69); // e setTimeout(() => { const ac = hot.getActiveEditor(); const innerHot = ac.htEditor; expect(innerHot.getData()).toEqual([ ['red'], ['yellow'], ['green'], ['blue'], ['lime'], ['white'], ['olive'], ['orange'], ['purple'] ]); editorInput.val('E'); keyDownUp(69); // E (same as 'e') }, 50); setTimeout(() => { const ac = hot.getActiveEditor(); const innerHot = ac.htEditor; expect(innerHot.getData()).toEqual([]); expect(innerHot.getSourceData()).toEqual([]); done(); }, 200); }); it('typing in textarea should NOT filter the lookup list when filtering is disabled', (done) => { const hot = handsontable({ columns: [ { editor: 'autocomplete', source: choices, filter: false } ] }); selectCell(0, 0); const editorInput = $('.handsontableInput'); expect(getDataAtCell(0, 0)).toBeNull(); keyDownUp('enter'); setTimeout(() => { editorInput.val('e'); keyDownUp('e'.charCodeAt(0)); // e }, 20); setTimeout(() => { const ac = hot.getActiveEditor(); const innerHot = ac.htEditor; expect(innerHot.getData()).toEqual(Handsontable.helper.pivot([choices])); editorInput.val('ed'); keyDownUp('d'.charCodeAt(0)); // d }, 40); setTimeout(() => { const ac = hot.getActiveEditor(); const innerHot = ac.htEditor; expect(innerHot.getData()).toEqual(Handsontable.helper.pivot([choices])); done(); }, 60); }); it('typing in textarea should highlight the matching phrase', (done) => { const choicesList = ['Male', 'Female']; const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(choicesList.filter(choice => choice.search(new RegExp(query, 'i')) !== -1)); }); const hot = handsontable({ columns: [ { editor: 'autocomplete', source: syncSources, filter: false } ] }); selectCell(0, 0); const editorInput = $('.handsontableInput'); expect(getDataAtCell(0, 0)).toBeNull(); keyDownUp('enter'); setTimeout(() => { syncSources.calls.reset(); editorInput.val('Male'); keyDownUp(69); // e }, 200); setTimeout(() => { const ac = hot.getActiveEditor(); const innerHot = ac.htEditor; const autocompleteList = $(innerHot.rootElement); expect(autocompleteList.find('td:eq(0)').html()).toMatch(/<(strong|STRONG)>Male<\/(strong|STRONG)>/); // IE8 makes the tag names UPPERCASE expect(autocompleteList.find('td:eq(1)').html()).toMatch(/Fe<(strong|STRONG)>male<\/(strong|STRONG)>/); done(); }, 400); }); it('text in textarea should not be interpreted as regexp', (done) => { spyOn(Handsontable.editors.AutocompleteEditor.prototype, 'queryChoices').and.callThrough(); const queryChoices = Handsontable.editors.AutocompleteEditor.prototype.queryChoices; const hot = handsontable({ columns: [ { editor: 'autocomplete', source: choices } ] }); selectCell(0, 0); const editorInput = $('.handsontableInput'); expect(getDataAtCell(0, 0)).toBeNull(); keyDownUp('enter'); setTimeout(() => { queryChoices.calls.reset(); editorInput.val('yellow|red'); keyDownUp('d'.charCodeAt(0)); }, 200); setTimeout(() => { const ac = hot.getActiveEditor(); const innerHot = ac.htEditor; expect(innerHot.getData().length).toEqual(0); done(); }, 400); }); it('text in textarea should not be interpreted as regexp when highlighting the matching phrase', (done) => { const choicesList = ['Male', 'Female']; const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(choicesList.filter(choice => choice.search(new RegExp(query, 'i')) !== -1)); }); const hot = handsontable({ columns: [ { editor: 'autocomplete', source: syncSources, filter: false } ] }); selectCell(0, 0); const editorInput = $('.handsontableInput'); expect(getDataAtCell(0, 0)).toBeNull(); keyDownUp('enter'); setTimeout(() => { syncSources.calls.reset(); editorInput.val('M|F'); keyDownUp('F'.charCodeAt(0)); }, 200); setTimeout(() => { const ac = hot.getActiveEditor(); const innerHot = ac.htEditor; const autocompleteList = $(innerHot.rootElement); expect(autocompleteList.find('td:eq(0)').html()).toEqual('Male'); expect(autocompleteList.find('td:eq(1)').html()).toEqual('Female'); done(); }, 400); }); it('should allow any value if filter === false and allowInvalid === true', (done) => { spyOn(Handsontable.editors.AutocompleteEditor.prototype, 'queryChoices').and.callThrough(); const queryChoices = Handsontable.editors.AutocompleteEditor.prototype.queryChoices; handsontable({ columns: [ { editor: 'autocomplete', source: choices, filter: false, strict: true, allowInvalid: true } ] }); selectCell(0, 0); const editorInput = $('.handsontableInput'); expect(getDataAtCell(0, 0)).toBeNull(); keyDownUp('enter'); setTimeout(() => { queryChoices.calls.reset(); editorInput.val('foobar'); keyDownUp(82); // r }, 200); setTimeout(() => { keyDownUp(Handsontable.helper.KEY_CODES.ENTER); expect(getDataAtCell(0, 0)).toEqual('foobar'); done(); }, 400); }); it('typing in textarea should highlight best choice, if strict === true', (done) => { const choicesList = ['Male', 'Female']; const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(choicesList.filter(choice => choice.search(new RegExp(query, 'i')) !== -1)); }); const hot = handsontable({ columns: [ { editor: 'autocomplete', source: syncSources, filter: false, strict: true } ] }); selectCell(0, 0); const editorInput = $('.handsontableInput'); expect(getDataAtCell(0, 0)).toBeNull(); keyDownUp('enter'); setTimeout(() => { syncSources.calls.reset(); editorInput.val('e'); keyDownUp(69); // e }, 200); setTimeout(() => { const ac = hot.getActiveEditor(); const innerHot = ac.htEditor; expect(innerHot.getSelected()).toEqual([[1, 0, 1, 0]]); done(); }, 400); }); }); it('should restore the old value when hovered over a autocomplete menu item and then clicked outside of the table', (done) => { const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(choices); }); handsontable({ columns: [ { editor: 'autocomplete', source: syncSources } ] }); selectCell(0, 0); expect(getDataAtCell(0, 0)).toBeNull(); keyDownUp('enter'); setTimeout(() => { autocomplete().find('tbody td:eq(1)').simulate('mouseenter'); autocomplete().find('tbody td:eq(1)').simulate('mouseleave'); spec().$container.simulate('mousedown'); expect(getDataAtCell(0, 0)).toBeNull(); done(); }, 200); }); it('should be able to use empty value ("")', (done) => { const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(['', 'BMW', 'Bentley']); }); handsontable({ data: [ ['one', 'two'], ['three', 'four'] ], columns: [ { editor: 'autocomplete', source: syncSources, filter: false } ] }); selectCell(0, 0); keyDownUp('enter'); setTimeout(() => { expect(getDataAtCell(0, 0)).toEqual('one'); autocomplete().find('tbody td:eq(0)').simulate('mousedown'); expect(getDataAtCell(0, 0)).toEqual(''); done(); }, 200); }); describe('allow html mode', () => { it('should allow inject html items (async mode)', (done) => { const hot = handsontable({ columns: [ { type: 'autocomplete', source(query, cb) { cb(['foo zip', 'bar', 'baz']); }, allowHtml: true, } ] }); selectCell(0, 0); const editorInput = $('.handsontableInput'); expect(getDataAtCell(0, 0)).toBeNull(); keyDownUp('enter'); setTimeout(() => { editorInput.val('b'); keyDownUp('b'.charCodeAt(0)); }, 200); setTimeout(() => { const ac = hot.getActiveEditor(); const innerHot = ac.htEditor; expect(innerHot.getData()).toEqual([ ['bar'], ['baz'], ]); editorInput.val('bar'); keyDownUp('a'.charCodeAt(0)); keyDownUp('r'.charCodeAt(0)); }, 400); setTimeout(() => { const ac = hot.getActiveEditor(); const innerHot = ac.htEditor; expect(innerHot.getData()).toEqual([ ['bar'] ]); keyDownUp('arrow_down'); keyDownUp('enter'); }, 600); setTimeout(() => { expect(getCell(0, 0).querySelector('i').textContent).toBe('bar'); done(); }, 700); }); it('should allow inject html items (sync mode)', (done) => { const hot = handsontable({ columns: [ { type: 'autocomplete', source: ['foo zip', 'bar', 'baz'], allowHtml: true, } ] }); selectCell(0, 0); const editorInput = $('.handsontableInput'); expect(getDataAtCell(0, 0)).toBeNull(); keyDownUp('enter'); setTimeout(() => { editorInput.val('b'); keyDownUp('b'.charCodeAt(0)); }, 200); setTimeout(() => { const ac = hot.getActiveEditor(); const innerHot = ac.htEditor; expect(innerHot.getData()).toEqual([ ['bar'], ['baz'], ]); editorInput.val('bar'); keyDownUp('a'.charCodeAt(0)); keyDownUp('r'.charCodeAt(0)); }, 400); setTimeout(() => { const ac = hot.getActiveEditor(); const innerHot = ac.htEditor; expect(innerHot.getData()).toEqual([ ['bar'] ]); keyDownUp('arrow_down'); keyDownUp('enter'); }, 600); setTimeout(() => { expect(getCell(0, 0).querySelector('i').textContent).toBe('bar'); done(); }, 700); }); }); describe('disallow html mode', () => { it('should be disabled by default', () => { const hot = handsontable({ columns: [ { type: 'autocomplete', source(query, cb) { cb(['foo zip', 'bar', 'baz']); }, allowHtml: false, } ] }); expect(hot.getCellMeta(0, 0).allowHtml).toBeFalsy(); }); it('should strip html from strings provided in source (async mode)', async() => { const hot = handsontable({ columns: [ { type: 'autocomplete', source(query, cb) { cb(['foo zip', 'bar', 'baz']); }, allowHtml: false, } ] }); selectCell(0, 0); const editorInput = $('.handsontableInput'); expect(getDataAtCell(0, 0)).toBeNull(); keyDownUp('enter'); await sleep(200); editorInput.val('b'); keyDownUp('b'.charCodeAt(0)); await sleep(200); { const ac = hot.getActiveEditor(); const innerHot = ac.htEditor; expect(innerHot.getData()).toEqual([ ['bar'], ['baz'], ]); editorInput.val('bar'); keyDownUp('a'.charCodeAt(0)); keyDownUp('r'.charCodeAt(0)); } await sleep(200); { const ac = hot.getActiveEditor(); const innerHot = ac.htEditor; expect(innerHot.getData()).toEqual([ ['bar'] ]); keyDownUp('arrow_down'); keyDownUp('enter'); } await sleep(200); expect(getCell(0, 0).querySelector('i')).toBeNull(); expect(getCell(0, 0).textContent).toMatch('bar'); }); it('should strip html from strings provided in source (sync mode)', async() => { const hot = handsontable({ columns: [ { type: 'autocomplete', source: ['foo zip', 'bar', 'baz'], allowHtml: false, } ] }); selectCell(0, 0); const editorInput = $('.handsontableInput'); expect(getDataAtCell(0, 0)).toBeNull(); keyDownUp('enter'); await sleep(200); editorInput.val('b'); keyDownUp('b'.charCodeAt(0)); await sleep(200); { const ac = hot.getActiveEditor(); const innerHot = ac.htEditor; expect(innerHot.getData()).toEqual([ ['bar'], ['baz'], ]); } editorInput.val('bar'); keyDownUp('a'.charCodeAt(0)); keyDownUp('r'.charCodeAt(0)); await sleep(200); { const ac = hot.getActiveEditor(); const innerHot = ac.htEditor; expect(innerHot.getData()).toEqual([ ['bar'] ]); } keyDownUp('arrow_down'); keyDownUp('enter'); await sleep(100); expect(getCell(0, 0).querySelector('i')).toBeNull(); expect(getCell(0, 0).textContent).toMatch('bar'); }); }); describe('Autocomplete helper functions:', () => { describe('sortByRelevance', () => { it('should sort the provided array, so items more relevant to the provided value are listed first', () => { const choicesList = [ 'Wayne', // 0 'Draven', // 1 'Banner', // 2 'Stark', // 3 'Parker', // 4 'Kent', // 5 'Gordon', // 6 'Kyle', // 7 'Simmons'// 8 ]; let value = 'a'; let sorted = Handsontable.editors.AutocompleteEditor.sortByRelevance(value, choicesList); expect(sorted).toEqual([0, 2, 4, 3, 1]); value = 'o'; sorted = Handsontable.editors.AutocompleteEditor.sortByRelevance(value, choicesList); expect(sorted).toEqual([6, 8]); value = 'er'; sorted = Handsontable.editors.AutocompleteEditor.sortByRelevance(value, choicesList); expect(sorted).toEqual([2, 4]); }); }); }); it('should not modify the suggestion lists\' order, when the sortByRelevance option is set to false', (done) => { const choicesList = [ 'Wayne', 'Draven', 'Banner', 'Stark', 'Parker', 'Kent', 'Gordon', 'Kyle', 'Simmons' ]; handsontable({ columns: [ { editor: 'autocomplete', source: choicesList, sortByRelevance: false } ] }); selectCell(0, 0); keyDownUp('enter'); const $editorInput = $('.handsontableInput'); $editorInput.val('a'); keyDownUp('a'.charCodeAt(0)); Handsontable.dom.setCaretPosition($editorInput[0], 1); setTimeout(() => { const dropdownList = $('.autocompleteEditor tbody').first(); const listLength = dropdownList.find('tr').size(); expect(listLength).toBe(9); for (let i = 1; i <= listLength; i++) { expect(dropdownList.find(`tr:nth-child(${i}) td`).text()).toEqual(choicesList[i - 1]); } done(); }, 30); }); it('should fire one afterChange event when value is changed', (done) => { const onAfterChange = jasmine.createSpy('onAfterChange'); const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(choices); }); handsontable({ columns: [ { editor: 'autocomplete', source: syncSources } ], afterChange: onAfterChange }); selectCell(0, 0); keyDownUp('enter'); setTimeout(() => { onAfterChange.calls.reset(); autocomplete().find('tbody td:eq(1)').simulate('mousedown'); expect(getDataAtCell(0, 0)).toEqual('red'); expect(onAfterChange.calls.count()).toEqual(1); expect(onAfterChange).toHaveBeenCalledWith([[0, 0, null, 'red']], 'edit', undefined, undefined, undefined, undefined); done(); }, 200); }); it('should not affect other cell values after clicking on autocomplete cell (#1021)', (done) => { const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(choices); }); handsontable({ columns: [ {}, {}, { editor: 'autocomplete', source: syncSources }, {} ], data: [ [null, null, 'yellow', null], [null, null, 'red', null], [null, null, 'blue', null] ] }); expect($(getCell(0, 2)).text()).toMatch('yellow'); mouseDoubleClick(getCell(0, 2)); expect($(getCell(1, 2)).text()).toMatch('red'); mouseDoubleClick(getCell(1, 2)); expect($(getCell(2, 2)).text()).toMatch('blue'); mouseDoubleClick(getCell(2, 2)); setTimeout(() => { expect(getDataAtCol(2)).toEqual(['yellow', 'red', 'blue']); done(); }, 200); }); it('should handle editor if cell data is a function', (done) => { spyOn(Handsontable.editors.AutocompleteEditor.prototype, 'updateChoicesList').and.callThrough(); const afterValidateCallback = jasmine.createSpy('afterValidateCallbak'); const hot = handsontable({ data: [ new Model({ id: 1, name: 'Ted Right', address: '' }), new Model({ id: 2, name: 'Frank Honest', address: '' }), new Model({ id: 3, name: 'Joan Well', address: '' })], dataSchema: Model, colHeaders: ['ID', 'Name', 'Address'], columns: [ { data: createAccessorForProperty('id'), type: 'autocomplete', source: ['1', '2', '3'], filter: false, strict: true }, { data: createAccessorForProperty('name') }, { data: createAccessorForProperty('address') } ], minSpareRows: 1, afterValidate: afterValidateCallback }); selectCell(0, 0); expect(hot.getActiveEditor().isOpened()).toBe(false); keyDownUp('enter'); setTimeout(() => { expect(hot.getActiveEditor().isOpened()).toBe(true); afterValidateCallback.calls.reset(); $(hot.getActiveEditor().htContainer).find('tr:eq(1) td:eq(0)').simulate('mousedown'); }, 200); setTimeout(() => { expect(getDataAtCell(0, 0)).toEqual('2'); done(); }, 400); }); // Input element should be focused on cell selection othrwise it breaks IME editor functionality for Asian users. it('should not lose the focus on input element while inserting new characters (#839)', async() => { const focusListener = jasmine.createSpy('focus'); const hot = handsontable({ data: [ ['one', 'two'], ['three', 'four'] ], columns: [ { type: 'autocomplete', source: choices, }, {}, ], }); selectCell(0, 0); hot.getActiveEditor().TEXTAREA.addEventListener('focus', focusListener); await sleep(50); expect(focusListener).toHaveBeenCalled(); hot.getActiveEditor().TEXTAREA.removeEventListener('focus', focusListener); }); it('should not lose the focus from the editor after selecting items from the choice list', async() => { const hot = handsontable({ data: [ ['', 'two'], ['three', 'four'] ], columns: [ { type: 'autocomplete', source: ['brown', 'yellow', 'green'], }, {}, ], }); selectCell(0, 0); keyDownUp('enter'); await sleep(0); keyDownUp('arrow_down'); keyDownUp('arrow_down'); keyDownUp('arrow_down'); hot.getActiveEditor().TEXTAREA.value = 'r'; keyDownUp('R'.charCodeAt(0)); await sleep(0); // Check if ESCAPE key is responsive. keyDownUp('esc'); expect(hot.isListening()).toBeTruthy(); expect(isEditorVisible($(hot.getActiveEditor().htEditor.rootElement))).toBeFalsy(); }); it('should not call the `source` has been selected', () => { const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process([]); // hardcoded empty result }); handsontable({ data: [ ['one', 'two'], ['three', 'four'] ], columns: [ { type: 'autocomplete', source: syncSources, allowInvalid: false, strict: true }, {} ], cells(row, col) { const cellProperties = {}; if (row === 0 && col === 0) { cellProperties.readOnly = true; } return cellProperties; } }); expect(getCellMeta(0, 0).readOnly).toBe(true); expect(syncSources).not.toHaveBeenCalled(); selectCell(0, 0); expect(syncSources).not.toHaveBeenCalled(); expect(getCellMeta(1, 0).readOnly).toBeFalsy(); selectCell(1, 0); expect(syncSources).not.toHaveBeenCalled(); }); it('should not call the `source` method if cell is read only and the arrow has been clicked', (done) => { const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process([]); // hardcoded empty result }); handsontable({ data: [ ['one', 'two'], ['three', 'four'] ], columns: [ { type: 'autocomplete', source: syncSources, allowInvalid: false, strict: true }, {} ], cells(row, col) { const cellProperties = {}; if (row === 0 && col === 0) { cellProperties.readOnly = true; } return cellProperties; } }); expect(getCellMeta(0, 0).readOnly).toBe(true); expect(syncSources).not.toHaveBeenCalled(); selectCell(0, 0); $(getCell(0, 0)).find('.htAutocompleteArrow').simulate('mousedown'); setTimeout(() => { expect(syncSources).not.toHaveBeenCalled(); syncSources.calls.reset(); expect(getCellMeta(1, 0).readOnly).toBeFalsy(); selectCell(1, 0); $(getCell(1, 0)).find('.htAutocompleteArrow').simulate('mousedown'); }, 100); setTimeout(() => { expect(syncSources).toHaveBeenCalled(); expect(syncSources.calls.count()).toEqual(1); done(); }, 200); }); it('should add a scrollbar to the autocomplete dropdown, only if number of displayed choices exceeds 10', async() => { const hot = handsontable({ data: [ ['', 'two', 'three'], ['four', 'five', 'six'] ], columns: [ { type: 'autocomplete', source: choices, allowInvalid: false, strict: false }, {}, {} ] }); spec().$container.css({ height: 600 }); expect(choices.length).toBeGreaterThan(10); selectCell(0, 0); $(getCell(0, 0)).find('.htAutocompleteArrow').simulate('mousedown'); const dropdownHolder = hot.getActiveEditor().htEditor.view.wt.wtTable.holder; await sleep(30); expect(dropdownHolder.scrollHeight).toBeGreaterThan(dropdownHolder.clientHeight); keyDownUp('esc'); hot.getSettings().columns[0].source = hot.getSettings().columns[0].source.slice(0).splice(3); hot.updateSettings({}); selectCell(0, 0); $(getCell(0, 0)).find('.htAutocompleteArrow').simulate('mousedown'); await sleep(30); expect(dropdownHolder.scrollHeight > dropdownHolder.clientHeight).toBe(false); }); it('should not close editor on scrolling', async() => { const hot = handsontable({ data: [ ['', 'two', 'three'], ['four', 'five', 'six'] ], columns: [ { type: 'autocomplete', source: choices, allowInvalid: false, strict: false }, {}, {} ] }); expect(choices.length).toBeGreaterThan(10); selectCell(0, 0); $(getCell(0, 0)).find('.htAutocompleteArrow').simulate('mousedown'); $(getCell(0, 0)).find('.htAutocompleteArrow').simulate('mouseup'); const dropdown = hot.getActiveEditor().htContainer; hot.view.wt.wtOverlays.topOverlay.scrollTo(1); await sleep(50); expect($(dropdown).is(':visible')).toBe(true); selectCell(0, 0); await sleep(50); $(getCell(0, 0)).find('.htAutocompleteArrow').simulate('mousedown'); $(getCell(0, 0)).find('.htAutocompleteArrow').simulate('mouseup'); hot.view.wt.wtOverlays.topOverlay.scrollTo(3); await sleep(50); expect($(dropdown).is(':visible')).toBe(true); }); it('should keep textarea caret position, after moving the selection to the suggestion list (pressing down arrow)', async() => { const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(choices.filter(choice => choice.indexOf(query) !== -1)); }); handsontable({ columns: [ { type: 'autocomplete', source: syncSources, strict: false } ] }); selectCell(0, 0); keyDownUp('enter'); const $editorInput = $('.handsontableInput'); $editorInput.val('an'); keyDownUp(65); // a keyDownUp(78); // n await sleep(0); Handsontable.dom.setCaretPosition($editorInput[0], 1); await sleep(200); keyDownUp('arrow_down'); expect(Handsontable.dom.getCaretPosition($editorInput[0])).toEqual(1); keyDownUp('arrow_down'); expect(Handsontable.dom.getCaretPosition($editorInput[0])).toEqual(1); }); it('should keep textarea selection, after moving the selection to the suggestion list (pressing down arrow)', async() => { const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(choices.filter(choice => choice.indexOf(query) !== -1)); }); handsontable({ columns: [ { type: 'autocomplete', source: syncSources, strict: false } ] }); selectCell(0, 0); keyDownUp('enter'); const $editorInput = $('.handsontableInput'); $editorInput.val('an'); keyDownUp(65); // a keyDownUp(78); // n await sleep(0); Handsontable.dom.setCaretPosition($editorInput[0], 1, 2); await sleep(200); keyDownUp('arrow_down'); expect(Handsontable.dom.getCaretPosition($editorInput[0])).toEqual(1); expect(Handsontable.dom.getSelectionEndPosition($editorInput[0])).toEqual(2); keyDownUp('arrow_down'); expect(Handsontable.dom.getCaretPosition($editorInput[0])).toEqual(1); expect(Handsontable.dom.getSelectionEndPosition($editorInput[0])).toEqual(2); }); it('should jump to the sibling cell, after pressing up key in quick edit mode', (done) => { const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(choices.filter(choice => choice.indexOf(query) !== -1)); }); handsontable({ columns: [ { type: 'autocomplete', source: syncSources, strict: false }, {} ] }); selectCell(1, 0); keyDownUp('x'); // trigger quick edit mode const $editorInput = $('.handsontableInput'); $editorInput.val('an'); keyDownUp(65); // a keyDownUp(78); // n setTimeout(() => { keyDownUp('arrow_up'); expect(getSelected()).toEqual([[0, 0, 0, 0]]); done(); }, 200); }); it('should jump to the next cell, after pressing right key in quick edit mode', (done) => { const syncSources = jasmine.createSpy('syncSources'); syncSources.plan = function(query, process) { process(choices.filter(choice => choice.indexOf(query) !== -1)); }; handsontable({ columns: [ { type: 'autocomplete', source: syncSources, strict: false }, {} ] }); selectCell(1, 0); keyDownUp('x'); // trigger quick edit mode const $editorInput = $('.handsontableInput'); $editorInput.val('an'); keyDownUp(65); // a keyDownUp(78); // n setTimeout(() => { keyDownUp('arrow_right'); expect(getSelected()).toEqual([[1, 1, 1, 1]]); done(); }, 200); }); it('should jump to the next cell, after pressing left key in quick edit mode', (done) => { const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(choices.filter(choice => choice.indexOf(query) !== -1)); }); handsontable({ columns: [ {}, { type: 'autocomplete', source: syncSources, strict: false } ] }); selectCell(1, 1); keyDownUp('x'); // trigger quick edit mode const $editorInput = $('.handsontableInput'); $editorInput.val('an'); keyDownUp(65); // a keyDownUp(78); // n // put caret on the end of the text to ensure that editor will be closed after hit left arrow key Handsontable.dom.setCaretPosition($editorInput[0], 2, 2); setTimeout(() => { keyDownUp('arrow_left'); expect(getSelected()).toEqual([[1, 0, 1, 0]]); done(); }, 200); }); it('should jump to the next cell, after pressing down key in quick edit mode', (done) => { const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(choices.filter(choice => choice.indexOf(query) !== -1)); }); handsontable({ columns: [ { type: 'autocomplete', source: syncSources, strict: false }, {} ] }); selectCell(1, 0); keyDownUp('x'); // trigger quick edit mode const $editorInput = $('.handsontableInput'); $editorInput.val('an'); keyDownUp(65); // a keyDownUp(78); // n setTimeout(() => { keyDownUp('arrow_down'); expect(getSelected()).toEqual([[1, 0, 1, 0]]); done(); }, 200); }); it('should jump to the next cell, after pressing down key in quick edit mode when no matching option list found', (done) => { const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(choices.filter(choice => choice.indexOf(query) !== -1)); }); handsontable({ columns: [ { type: 'autocomplete', source: syncSources, strict: false }, {} ] }); selectCell(1, 0); keyDownUp('x'); // trigger quick edit mode const $editorInput = $('.handsontableInput'); $editorInput.val('anananan'); keyDownUp(65); // a keyDownUp(78); // n keyDownUp(65); // a keyDownUp(78); // n keyDownUp(65); // a keyDownUp(78); // n keyDownUp(65); // a keyDownUp(78); // n setTimeout(() => { keyDownUp('arrow_down'); expect(getSelected()).toEqual([[2, 0, 2, 0]]); done(); }, 200); }); it('should not jump to the next cell, after pressing down key in quick edit mode when options list was opened', (done) => { const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(choices.filter(choice => choice.indexOf(query) !== -1)); }); handsontable({ columns: [ { type: 'autocomplete', source: syncSources, strict: false }, {} ] }); selectCell(1, 0); keyDownUp('x'); // trigger quick edit mode const $editorInput = $('.handsontableInput'); $editorInput.val('an'); keyDownUp(65); // a keyDownUp(78); // n setTimeout(() => { keyDownUp('arrow_down'); expect(getSelected()).toEqual([[1, 0, 1, 0]]); done(); }, 200); }); it('should select option in opened editor after pressing down key in quick edit mode', (done) => { const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(choices.filter(choice => choice.indexOf(query) !== -1)); }); const hot = handsontable({ columns: [ { type: 'autocomplete', source: syncSources, strict: false }, {} ] }); selectCell(1, 0); keyDownUp('x'); // Trigger quick edit mode setTimeout(() => { keyDownUp('arrow_down'); expect(hot.getActiveEditor().htEditor.getSelected()).toEqual([[0, 0, 0, 0]]); keyDownUp('arrow_down'); expect(hot.getActiveEditor().htEditor.getSelected()).toEqual([[1, 0, 1, 0]]); keyDownUp('arrow_down'); expect(hot.getActiveEditor().htEditor.getSelected()).toEqual([[2, 0, 2, 0]]); done(); }, 200); }); it('should select option in opened editor after pressing up key in quick edit mode', (done) => { const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(choices.filter(choice => choice.indexOf(query) !== -1)); }); const hot = handsontable({ columns: [ { type: 'autocomplete', source: syncSources, strict: false }, {} ], autoWrapCol: false, autoWrapRow: false }); selectCell(1, 0); keyDownUp('x'); // Trigger quick edit mode setTimeout(() => { hot.getActiveEditor().htEditor.selectCell(2, 0); expect(hot.getActiveEditor().htEditor.getSelected()).toEqual([[2, 0, 2, 0]]); keyDownUp('arrow_up'); expect(hot.getActiveEditor().htEditor.getSelected()).toEqual([[1, 0, 1, 0]]); keyDownUp('arrow_up'); expect(hot.getActiveEditor().htEditor.getSelected()).toEqual([[0, 0, 0, 0]]); keyDownUp('arrow_up'); expect(hot.getActiveEditor().htEditor.getSelected()).toEqual([[0, 0, 0, 0]]); done(); }, 200); }); it('should not close editor in quick edit mode after pressing down key when last option is selected', (done) => { const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(choices.filter(choice => choice.indexOf(query) !== -1)); }); const hot = handsontable({ columns: [ { type: 'autocomplete', source: syncSources, strict: false }, {} ] }); selectCell(1, 0); keyDownUp('x'); // Trigger quick edit mode setTimeout(() => { hot.getActiveEditor().htEditor.selectCell(7, 0); hot.listen(); keyDownUp('arrow_down'); keyDownUp('arrow_down'); keyDownUp('arrow_down'); keyDownUp('arrow_down'); keyDownUp('arrow_down'); expect(hot.getActiveEditor().isOpened()).toBe(true); done(); }, 200); }); it('should close editor in quick edit mode after pressing up key when no option is selected', (done) => { const syncSources = jasmine.createSpy('syncSources'); syncSources.and.callFake((query, process) => { process(choices.filter(choice => choice.indexOf(query) !== -1)); }); const hot = handsontable({ columns: [ { type: 'autocomplete', source: syncSources, strict: false }, {} ] }); selectCell(1, 0); keyDownUp('x'); // Trigger quick edit mode setTimeout(() => { hot.getActiveEditor().htEditor.selectCell(1, 0); hot.listen(); keyDownUp('arrow_up'); keyDownUp('arrow_up'); keyDownUp('arrow_up'); expect(getSelected()).toEqual([[0, 0, 0, 0]]); done(); }, 200); }); describe('IME support', () => { it('should focus editable element after selecting the cell', async() => { handsontable({ columns: [ { editor: 'autocomplete', source: choices } ] }); selectCell(0, 0, 0, 0, true, false); await sleep(10); expect(document.activeElement).toBe(getActiveEditor().TEXTAREA); }); }); });