diff --git a/scripts/playground.html b/scripts/playground.html index 948e8a31..f99447b3 100644 --- a/scripts/playground.html +++ b/scripts/playground.html @@ -1,61 +1,51 @@ - - + + DocSearch playground - - - - - + + +
-
-

Search thanks to the assets served at http://127.0.0.1:8080/

-

Do not forget to hard refresh: cmd + maj + R

- -
-
- -
- -
-
-
-

2

- -
-
- -
- -
+
+

Assets are served from http://127.0.0.1:8080/

+
+
+
+
+
- - - - + - + diff --git a/scripts/serve b/scripts/serve index 15dc6477..fe939e63 100755 --- a/scripts/serve +++ b/scripts/serve @@ -3,9 +3,9 @@ yarn run build:css echo "Find all files at http://127.0.0.1:8080/"; -open scripts/playground.html parallelshell \ 'webpack --config webpack.serve.config.babel.js -w' \ 'onchange "./src/styles/*.scss" -- yarn run build:css' \ - 'live-server ./dist/cdn' \ + 'live-server --no-browser ./dist/cdn' \ + "${BROWSER} scripts/playground.html" diff --git a/scripts/test-watch b/scripts/test-watch index 63b84fce..429a46cc 100755 --- a/scripts/test-watch +++ b/scripts/test-watch @@ -3,5 +3,6 @@ set -e jest \ --no-cache \ --watch \ + --no-watchman \ ./src/lib/ diff --git a/src/lib/DocSearch.js b/src/lib/DocSearch.js index 1eaabefc..310d9ed3 100644 --- a/src/lib/DocSearch.js +++ b/src/lib/DocSearch.js @@ -64,7 +64,7 @@ class DocSearch { this.apiKey = apiKey; this.appId = appId; this.indexName = indexName; - this.inputs = DocSearch.getInputsFromSelector(inputSelector); + this.input = DocSearch.getInputFromSelector(inputSelector); this.algoliaOptions = { hitsPerPage: 5, ...algoliaOptions }; const autocompleteOptionsDebug = autocompleteOptions && autocompleteOptions.debug @@ -79,48 +79,50 @@ class DocSearch { this.autocompleteOptions.cssClasses.prefix || 'ds'; // eslint-disable-next-line no-param-reassign + handleSelected = handleSelected || this.handleSelected; + this.isSimpleLayout = layout === 'simple'; this.client = algoliasearch(this.appId, this.apiKey); this.client.addAlgoliaAgent(`docsearch.js ${version}`); - for (let i = 0; i < this.inputs.length; i++) { - let uniqInput = this.inputs[i]; - if (uniqInput) { - if (enhancedSearchInput) { - uniqInput = DocSearch.injectSearchBox(uniqInput); - } - this.autocomplete = autocomplete(uniqInput, autocompleteOptions, [ - { - source: this.getAutocompleteSource(transformData, queryHook), - templates: { - suggestion: DocSearch.getSuggestionTemplate(this.isSimpleLayout), - footer: templates.footer, - empty: DocSearch.getEmptyTemplate(), - }, - }, - ]); - if (handleSelected) { - let handleSelectedFn = null; - if (typeof handleSelected === 'function') { - handleSelectedFn = handleSelected; - } else { - handleSelectedFn = this.handleSelected; - } - this.autocomplete.on( - 'autocomplete:selected', - handleSelectedFn.bind(null, this.autocomplete.autocomplete) - ); - } - this.autocomplete.on( - 'autocomplete:shown', - this.handleShown.bind(null, uniqInput) - ); + if (enhancedSearchInput) { + this.input = DocSearch.injectSearchBox(this.input); + } - if (enhancedSearchInput) { - DocSearch.bindSearchBoxEvent(); - } - } + this.autocomplete = autocomplete(this.input, autocompleteOptions, [ + { + source: this.getAutocompleteSource(transformData, queryHook), + templates: { + suggestion: DocSearch.getSuggestionTemplate(this.isSimpleLayout), + footer: templates.footer, + empty: DocSearch.getEmptyTemplate(), + }, + }, + ]); + + // If user defined its own handleSelected, we prevent clicks on suggestions + // link to do anything + if (handleSelected) { + $('.algolia-autocomplete').on('click', '.ds-suggestions a', event => { + event.preventDefault(); + }); + } + + // Click on suggestions will follow the link, but keyboard navigation still + // need the handleSelected + this.autocomplete.on( + 'autocomplete:selected', + handleSelected.bind(null, this.autocomplete.autocomplete) + ); + + this.autocomplete.on( + 'autocomplete:shown', + this.handleShown.bind(null, this.input) + ); + + if (enhancedSearchInput) { + DocSearch.bindSearchBoxEvent(); } } @@ -143,7 +145,7 @@ class DocSearch { ); } - if (!DocSearch.getInputsFromSelector(args.inputSelector)) { + if (!DocSearch.getInputFromSelector(args.inputSelector)) { throw new Error( `Error: No input element in the page matches ${args.inputSelector}` ); @@ -179,26 +181,14 @@ class DocSearch { /** * Returns the matching input from a CSS selector, null if none matches - * @function getInputsFromSelector + * @function getInputFromSelector * @param {string} selector CSS selector that matches the search * input of the page * @returns {void} */ - static getInputsFromSelector(selector) { - if (!selector.length) { - return null; - } else { - const selectors = selector.split(','); - if (selectors.length === 1) { - const input = $(selector).filter('input'); - return input.length ? [$(input[0])] : null; - } else { - return selectors.map(s => { - const input = $(s).filter('input'); - return input.length ? $(input[0]) : null; - }); - } - } + static getInputFromSelector(selector) { + const input = $(selector).filter('input'); + return input.length ? $(input[0]) : null; } /** diff --git a/src/lib/__tests__/DocSearch-test.js b/src/lib/__tests__/DocSearch-test.js index e075e935..b435e278 100644 --- a/src/lib/__tests__/DocSearch-test.js +++ b/src/lib/__tests__/DocSearch-test.js @@ -46,7 +46,7 @@ describe('DocSearch', () => { }; sinon.spy(DocSearch, 'checkArguments'); - sinon.stub(DocSearch, 'getInputsFromSelector').returns(true); + sinon.stub(DocSearch, 'getInputFromSelector').returns(true); DocSearch.__Rewire__('algoliasearch', AlgoliaSearch); DocSearch.__Rewire__('autocomplete', AutoComplete); @@ -54,7 +54,7 @@ describe('DocSearch', () => { afterEach(() => { DocSearch.checkArguments.restore(); - DocSearch.getInputsFromSelector.restore(); + DocSearch.getInputFromSelector.restore(); DocSearch.__ResetDependency__('algoliasearch'); DocSearch.__ResetDependency__('autocomplete'); }); @@ -136,13 +136,13 @@ describe('DocSearch', () => { it('should pass the input element as an instance property', () => { // Given const options = defaultOptions; - DocSearch.getInputsFromSelector.returns($('foo')); + DocSearch.getInputFromSelector.returns($('foo')); // When const actual = new DocSearch(options); // Then - const $inputs = actual.inputs; + const $inputs = actual.input; expect($inputs.text()).toEqual('foo'); expect($inputs[0].tagName).toEqual('SPAN'); }); @@ -194,7 +194,7 @@ describe('DocSearch', () => { autocompleteOptions: { anOption: '44' }, }; const $input = $(''); - DocSearch.getInputsFromSelector.returns([$input]); + DocSearch.getInputFromSelector.returns($input); // When new DocSearch(options); @@ -211,7 +211,7 @@ describe('DocSearch', () => { }); it('should listen to the selected and shown event of autocomplete', () => { // Given - const options = { ...defaultOptions, handleSelected: true }; + const options = { ...defaultOptions, handleSelected() {} }; // When new DocSearch(options); @@ -229,8 +229,8 @@ describe('DocSearch', () => { }); afterEach(() => { - if (DocSearch.getInputsFromSelector.restore) { - DocSearch.getInputsFromSelector.restore(); + if (DocSearch.getInputFromSelector.restore) { + DocSearch.getInputFromSelector.restore(); } }); @@ -262,7 +262,7 @@ describe('DocSearch', () => { apiKey: 'apiKey', indexName: 'indexName', }; - sinon.stub(DocSearch, 'getInputsFromSelector').returns(false); + sinon.stub(DocSearch, 'getInputFromSelector').returns(false); // When expect(() => { @@ -271,10 +271,10 @@ describe('DocSearch', () => { }); }); - describe('getInputsFromSelector', () => { - let getInputsFromSelector; + describe('getInputFromSelector', () => { + let getInputFromSelector; beforeEach(() => { - getInputsFromSelector = DocSearch.getInputsFromSelector; + getInputFromSelector = DocSearch.getInputFromSelector; }); it('should return null if no element matches the selector', () => { @@ -282,7 +282,7 @@ describe('DocSearch', () => { const selector = '.i-do-not-exist > at #all'; // When - const actual = getInputsFromSelector(selector); + const actual = getInputFromSelector(selector); // Then expect(actual).toEqual(null); @@ -292,7 +292,7 @@ describe('DocSearch', () => { const selector = '.i-am-a-span'; // When - const actual = getInputsFromSelector(selector); + const actual = getInputFromSelector(selector); // Then expect(actual).toEqual(null); @@ -302,10 +302,10 @@ describe('DocSearch', () => { const selector = '#input'; // When - const actual = getInputsFromSelector(selector); + const actual = getInputFromSelector(selector); // Then - expect($.zepto.isZ(actual[0])).toBe(true); + expect($.zepto.isZ(actual)).toBe(true); }); }); @@ -390,13 +390,12 @@ describe('DocSearch', () => { }); describe('handleSelected', () => { - it('should change the location', () => { + it('should change the location if no handleSelected specified', () => { // Given const options = { apiKey: 'key', indexName: 'foo', inputSelector: '#input', - handleSelected: true, }; // When @@ -412,6 +411,45 @@ describe('DocSearch', () => { resolve(); }); }); + it('should call the custom handleSelected if defined', () => { + // Given + const customHandleSelected = jest.fn(); + const options = { + apiKey: 'key', + indexName: 'foo', + inputSelector: '#input', + handleSelected: customHandleSelected, + }; + const expectedInput = expect.objectContaining({ + open: expect.any(Function), + }); + const expectedEvent = expect.objectContaining({ + type: 'autocomplete:selected' + }); + const expectedSuggestion = expect.objectContaining({ + url: 'https://website.com/doc/page', + }); + + // When + const ds = new DocSearch(options); + ds.autocomplete.trigger('autocomplete:selected', { + url: 'https://website.com/doc/page', + }); + + return new Promise(resolve => { + expect(customHandleSelected).toHaveBeenCalledWith( + expectedInput, + expectedEvent, + expectedSuggestion, + ); + resolve(); + }); + }); + xit('should prevent all clicks on links if a custom handleSelected is specified', () => { + // TODO + // If handleSelected, we target one link, we manually trigger a click on + // it, and we check that preventDefault is called on the event + }); }); describe('handleShown', () => { diff --git a/src/lib/templates.js b/src/lib/templates.js index cbab5f2e..c2b60b48 100644 --- a/src/lib/templates.js +++ b/src/lib/templates.js @@ -10,7 +10,7 @@ const templates = { {{#isCategoryHeader}}${suggestionPrefix}__main{{/isCategoryHeader}} {{#isSubCategoryHeader}}${suggestionPrefix}__secondary{{/isSubCategoryHeader}} " - target="_blank" aria-label="Link to the result" + aria-label="Link to the result" href="{{{url}}}" >