Repository: WebDevStudios/wp-search-with-algolia Branch: main Commit: fc21bb652574 Files: 1011 Total size: 6.0 MB Directory structure: gitextract_z0wy0_zz/ ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── LICENSE.md │ └── MIT-LICENSE.md ├── .gitignore ├── .nvmrc ├── CHANGELOG.md ├── README.txt ├── algolia.php ├── classmap.php ├── composer.json ├── css/ │ ├── algolia-autocomplete.css │ ├── algolia-instantsearch.css │ └── index.php ├── includes/ │ ├── admin/ │ │ ├── class-algolia-admin-page-autocomplete.php │ │ ├── class-algolia-admin-page-native-search.php │ │ ├── class-algolia-admin-page-premium-support.php │ │ ├── class-algolia-admin-page-seo.php │ │ ├── class-algolia-admin-page-settings.php │ │ ├── class-algolia-admin-page-woocommerce.php │ │ ├── class-algolia-admin-template-notices.php │ │ ├── class-algolia-admin.php │ │ ├── css/ │ │ │ ├── algolia-admin.css │ │ │ └── index.php │ │ ├── fonts/ │ │ │ └── index.php │ │ ├── img/ │ │ │ └── index.php │ │ ├── index.php │ │ ├── js/ │ │ │ ├── algolia-admin.js │ │ │ ├── index.php │ │ │ ├── push-settings-button.js │ │ │ └── reindex-button.js │ │ └── partials/ │ │ ├── form-options-premium-support.php │ │ ├── form-options-seo.php │ │ ├── form-options-woocommerce.php │ │ ├── form-options.php │ │ ├── form-override-search-option.php │ │ ├── form-override-search-version-option.php │ │ ├── index.php │ │ ├── page-autocomplete-config.php │ │ ├── page-autocomplete.php │ │ └── page-search.php │ ├── class-algolia-api.php │ ├── class-algolia-autocomplete-config.php │ ├── class-algolia-cli.php │ ├── class-algolia-compatibility.php │ ├── class-algolia-plugin.php │ ├── class-algolia-scripts.php │ ├── class-algolia-search.php │ ├── class-algolia-settings.php │ ├── class-algolia-styles.php │ ├── class-algolia-template-loader.php │ ├── class-algolia-utils.php │ ├── factories/ │ │ ├── class-algolia-http-client-interface-factory.php │ │ ├── class-algolia-plugin-factory.php │ │ └── class-algolia-search-client-factory.php │ ├── index.php │ ├── indices/ │ │ ├── class-algolia-index-replica.php │ │ ├── class-algolia-index.php │ │ ├── class-algolia-posts-index.php │ │ ├── class-algolia-searchable-posts-index.php │ │ ├── class-algolia-terms-index.php │ │ ├── class-algolia-users-index.php │ │ └── index.php │ ├── utilities/ │ │ ├── class-algolia-health-panel.php │ │ ├── class-algolia-template-utils.php │ │ ├── class-algolia-update-messages.php │ │ └── class-algolia-version-utils.php │ └── watchers/ │ ├── class-algolia-changes-watcher.php │ ├── class-algolia-post-changes-watcher.php │ ├── class-algolia-term-changes-watcher.php │ ├── class-algolia-user-changes-watcher.php │ └── index.php ├── index.php ├── js/ │ ├── algoliasearch/ │ │ ├── README.md │ │ ├── dist/ │ │ │ ├── algoliasearch-lite.d.ts │ │ │ ├── algoliasearch-lite.esm.browser.js │ │ │ ├── algoliasearch-lite.umd.js │ │ │ ├── algoliasearch.cjs.js │ │ │ ├── algoliasearch.d.ts │ │ │ ├── algoliasearch.esm.browser.js │ │ │ └── algoliasearch.umd.js │ │ ├── index.d.ts │ │ ├── index.js │ │ ├── lite.d.ts │ │ ├── lite.js │ │ └── package.json │ ├── autocomplete-noconflict.js │ ├── autocomplete.js/ │ │ ├── CHANGELOG.md │ │ ├── CONTRIBUTING.md │ │ ├── Gruntfile.js │ │ ├── LICENSE │ │ ├── README.md │ │ ├── bower.json │ │ ├── dist/ │ │ │ ├── autocomplete.angular.js │ │ │ ├── autocomplete.jquery.js │ │ │ └── autocomplete.js │ │ ├── examples/ │ │ │ ├── basic.html │ │ │ ├── basic_angular.html │ │ │ ├── basic_jquery.html │ │ │ └── index.html │ │ ├── index.js │ │ ├── index_angular.js │ │ ├── index_jquery.js │ │ ├── karma.conf.js │ │ ├── package.json │ │ ├── scripts/ │ │ │ ├── netlify-deploy.js │ │ │ └── release.sh │ │ ├── src/ │ │ │ ├── angular/ │ │ │ │ └── directive.js │ │ │ ├── autocomplete/ │ │ │ │ ├── css.js │ │ │ │ ├── dataset.js │ │ │ │ ├── dropdown.js │ │ │ │ ├── event_bus.js │ │ │ │ ├── event_emitter.js │ │ │ │ ├── html.js │ │ │ │ ├── input.js │ │ │ │ └── typeahead.js │ │ │ ├── common/ │ │ │ │ ├── dom.js │ │ │ │ ├── parseAlgoliaClientVersion.js │ │ │ │ └── utils.js │ │ │ ├── jquery/ │ │ │ │ └── plugin.js │ │ │ ├── sources/ │ │ │ │ ├── hits.js │ │ │ │ ├── index.js │ │ │ │ └── popularIn.js │ │ │ └── standalone/ │ │ │ └── index.js │ │ ├── test/ │ │ │ ├── ci.sh │ │ │ ├── fixtures.js │ │ │ ├── helpers/ │ │ │ │ ├── mocks.js │ │ │ │ └── waits_for.js │ │ │ ├── integration/ │ │ │ │ ├── test.html │ │ │ │ └── test.js │ │ │ ├── playground.css │ │ │ ├── playground.html │ │ │ ├── playground_angular.html │ │ │ ├── playground_jquery.html │ │ │ ├── test.bundle.js │ │ │ └── unit/ │ │ │ ├── angular_spec.js │ │ │ ├── dataset_spec.js │ │ │ ├── dropdown_spec.js │ │ │ ├── event_emitter_spec.js │ │ │ ├── hits_spec.js │ │ │ ├── input_spec.js │ │ │ ├── jquery_spec.js │ │ │ ├── parseAlgoliaClientVersion_spec.js │ │ │ ├── popularIn_spec.js │ │ │ ├── standalone_spec.js │ │ │ ├── typeahead_spec.js │ │ │ └── utils_spec.js │ │ ├── version.js │ │ └── zepto.js │ └── instantsearch.js/ │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── cjs/ │ │ ├── components/ │ │ │ ├── Answers/ │ │ │ │ └── Answers.js │ │ │ ├── Breadcrumb/ │ │ │ │ └── Breadcrumb.js │ │ │ ├── ClearRefinements/ │ │ │ │ └── ClearRefinements.js │ │ │ ├── CurrentRefinements/ │ │ │ │ └── CurrentRefinements.js │ │ │ ├── GeoSearchControls/ │ │ │ │ ├── GeoSearchButton.js │ │ │ │ ├── GeoSearchControls.js │ │ │ │ └── GeoSearchToggle.js │ │ │ ├── Highlight/ │ │ │ │ └── Highlight.js │ │ │ ├── Hits/ │ │ │ │ └── Hits.js │ │ │ ├── InfiniteHits/ │ │ │ │ └── InfiniteHits.js │ │ │ ├── InternalHighlight/ │ │ │ │ └── InternalHighlight.js │ │ │ ├── MenuSelect/ │ │ │ │ └── MenuSelect.js │ │ │ ├── Pagination/ │ │ │ │ ├── Pagination.js │ │ │ │ └── PaginationLink.js │ │ │ ├── Panel/ │ │ │ │ └── Panel.js │ │ │ ├── PoweredBy/ │ │ │ │ └── PoweredBy.js │ │ │ ├── QueryRuleCustomData/ │ │ │ │ └── QueryRuleCustomData.js │ │ │ ├── RangeInput/ │ │ │ │ └── RangeInput.js │ │ │ ├── RefinementList/ │ │ │ │ ├── RefinementList.js │ │ │ │ └── RefinementListItem.js │ │ │ ├── RelevantSort/ │ │ │ │ └── RelevantSort.js │ │ │ ├── ReverseHighlight/ │ │ │ │ └── ReverseHighlight.js │ │ │ ├── ReverseSnippet/ │ │ │ │ └── ReverseSnippet.js │ │ │ ├── SearchBox/ │ │ │ │ └── SearchBox.js │ │ │ ├── Selector/ │ │ │ │ └── Selector.js │ │ │ ├── Slider/ │ │ │ │ ├── Pit.js │ │ │ │ ├── Rheostat.js │ │ │ │ └── Slider.js │ │ │ ├── Snippet/ │ │ │ │ └── Snippet.js │ │ │ ├── Stats/ │ │ │ │ └── Stats.js │ │ │ ├── Template/ │ │ │ │ └── Template.js │ │ │ ├── ToggleRefinement/ │ │ │ │ └── ToggleRefinement.js │ │ │ └── VoiceSearch/ │ │ │ └── VoiceSearch.js │ │ ├── connectors/ │ │ │ ├── answers/ │ │ │ │ └── connectAnswers.js │ │ │ ├── autocomplete/ │ │ │ │ └── connectAutocomplete.js │ │ │ ├── breadcrumb/ │ │ │ │ └── connectBreadcrumb.js │ │ │ ├── clear-refinements/ │ │ │ │ └── connectClearRefinements.js │ │ │ ├── configure/ │ │ │ │ └── connectConfigure.js │ │ │ ├── configure-related-items/ │ │ │ │ └── connectConfigureRelatedItems.js │ │ │ ├── current-refinements/ │ │ │ │ └── connectCurrentRefinements.js │ │ │ ├── dynamic-widgets/ │ │ │ │ └── connectDynamicWidgets.js │ │ │ ├── frequently-bought-together/ │ │ │ │ └── connectFrequentlyBoughtTogether.js │ │ │ ├── geo-search/ │ │ │ │ └── connectGeoSearch.js │ │ │ ├── hierarchical-menu/ │ │ │ │ └── connectHierarchicalMenu.js │ │ │ ├── hits/ │ │ │ │ ├── connectHits.js │ │ │ │ └── connectHitsWithInsights.js │ │ │ ├── hits-per-page/ │ │ │ │ └── connectHitsPerPage.js │ │ │ ├── index.js │ │ │ ├── infinite-hits/ │ │ │ │ ├── connectInfiniteHits.js │ │ │ │ └── connectInfiniteHitsWithInsights.js │ │ │ ├── looking-similar/ │ │ │ │ └── connectLookingSimilar.js │ │ │ ├── menu/ │ │ │ │ └── connectMenu.js │ │ │ ├── numeric-menu/ │ │ │ │ └── connectNumericMenu.js │ │ │ ├── pagination/ │ │ │ │ ├── Paginator.js │ │ │ │ └── connectPagination.js │ │ │ ├── powered-by/ │ │ │ │ └── connectPoweredBy.js │ │ │ ├── query-rules/ │ │ │ │ └── connectQueryRules.js │ │ │ ├── range/ │ │ │ │ └── connectRange.js │ │ │ ├── rating-menu/ │ │ │ │ └── connectRatingMenu.js │ │ │ ├── refinement-list/ │ │ │ │ └── connectRefinementList.js │ │ │ ├── related-products/ │ │ │ │ └── connectRelatedProducts.js │ │ │ ├── relevant-sort/ │ │ │ │ └── connectRelevantSort.js │ │ │ ├── search-box/ │ │ │ │ └── connectSearchBox.js │ │ │ ├── sort-by/ │ │ │ │ └── connectSortBy.js │ │ │ ├── stats/ │ │ │ │ └── connectStats.js │ │ │ ├── toggle-refinement/ │ │ │ │ ├── connectToggleRefinement.js │ │ │ │ └── types.js │ │ │ ├── trending-items/ │ │ │ │ └── connectTrendingItems.js │ │ │ └── voice-search/ │ │ │ └── connectVoiceSearch.js │ │ ├── helpers/ │ │ │ ├── components/ │ │ │ │ ├── Highlight.js │ │ │ │ ├── ReverseHighlight.js │ │ │ │ ├── ReverseSnippet.js │ │ │ │ ├── Snippet.js │ │ │ │ └── index.js │ │ │ ├── get-insights-anonymous-user-token.js │ │ │ ├── highlight.js │ │ │ ├── index.js │ │ │ ├── insights.js │ │ │ ├── reverseHighlight.js │ │ │ ├── reverseSnippet.js │ │ │ └── snippet.js │ │ ├── index.js │ │ ├── lib/ │ │ │ ├── InstantSearch.js │ │ │ ├── createHelpers.js │ │ │ ├── formatNumber.js │ │ │ ├── infiniteHitsCache/ │ │ │ │ ├── index.js │ │ │ │ └── sessionStorage.js │ │ │ ├── insights/ │ │ │ │ ├── client.js │ │ │ │ ├── index.js │ │ │ │ └── listener.js │ │ │ ├── main.js │ │ │ ├── routers/ │ │ │ │ ├── history.js │ │ │ │ └── index.js │ │ │ ├── server.js │ │ │ ├── stateMappings/ │ │ │ │ ├── index.js │ │ │ │ ├── simple.js │ │ │ │ └── singleIndex.js │ │ │ ├── suit.js │ │ │ ├── templating/ │ │ │ │ ├── index.js │ │ │ │ ├── prepareTemplateProps.js │ │ │ │ └── renderTemplate.js │ │ │ ├── utils/ │ │ │ │ ├── addWidgetId.js │ │ │ │ ├── capitalize.js │ │ │ │ ├── checkIndexUiState.js │ │ │ │ ├── checkRendering.js │ │ │ │ ├── clearRefinements.js │ │ │ │ ├── concatHighlightedParts.js │ │ │ │ ├── convertNumericRefinementsToFilters.js │ │ │ │ ├── createConcurrentSafePromise.js │ │ │ │ ├── createSendEventForFacet.js │ │ │ │ ├── createSendEventForHits.js │ │ │ │ ├── cx.js │ │ │ │ ├── debounce.js │ │ │ │ ├── defer.js │ │ │ │ ├── detect-insights-client.js │ │ │ │ ├── documentation.js │ │ │ │ ├── escape-highlight.js │ │ │ │ ├── escape-html.js │ │ │ │ ├── escape.js │ │ │ │ ├── escapeFacetValue.js │ │ │ │ ├── escapeRefinement.js │ │ │ │ ├── find.js │ │ │ │ ├── findIndex.js │ │ │ │ ├── geo-search.js │ │ │ │ ├── getAppIdAndApiKey.js │ │ │ │ ├── getContainerNode.js │ │ │ │ ├── getHighlightFromSiblings.js │ │ │ │ ├── getHighlightedParts.js │ │ │ │ ├── getObjectType.js │ │ │ │ ├── getPropertyByPath.js │ │ │ │ ├── getRefinements.js │ │ │ │ ├── getWidgetAttribute.js │ │ │ │ ├── hits-absolute-position.js │ │ │ │ ├── hits-query-id.js │ │ │ │ ├── hydrateRecommendCache.js │ │ │ │ ├── hydrateSearchClient.js │ │ │ │ ├── index.js │ │ │ │ ├── isDomElement.js │ │ │ │ ├── isEqual.js │ │ │ │ ├── isFacetRefined.js │ │ │ │ ├── isFiniteNumber.js │ │ │ │ ├── isIndexWidget.js │ │ │ │ ├── isPlainObject.js │ │ │ │ ├── isSpecialClick.js │ │ │ │ ├── logger.js │ │ │ │ ├── mergeSearchParameters.js │ │ │ │ ├── noop.js │ │ │ │ ├── omit.js │ │ │ │ ├── prepareTemplateProps.js │ │ │ │ ├── range.js │ │ │ │ ├── render-args.js │ │ │ │ ├── renderTemplate.js │ │ │ │ ├── resolveSearchParameters.js │ │ │ │ ├── reverseHighlightedParts.js │ │ │ │ ├── safelyRunOnBrowser.js │ │ │ │ ├── serializer.js │ │ │ │ ├── setIndexHelperState.js │ │ │ │ ├── toArray.js │ │ │ │ ├── typedObject.js │ │ │ │ ├── unescape.js │ │ │ │ ├── unescapeRefinement.js │ │ │ │ ├── uniq.js │ │ │ │ ├── uuid.js │ │ │ │ └── walkIndex.js │ │ │ ├── version.js │ │ │ └── voiceSearchHelper/ │ │ │ ├── index.js │ │ │ └── types.js │ │ ├── middlewares/ │ │ │ ├── createInsightsMiddleware.js │ │ │ ├── createMetadataMiddleware.js │ │ │ ├── createRouterMiddleware.js │ │ │ └── index.js │ │ ├── templates/ │ │ │ ├── carousel/ │ │ │ │ └── carousel.js │ │ │ └── index.js │ │ ├── types/ │ │ │ ├── algoliasearch.js │ │ │ ├── component.js │ │ │ ├── connector.js │ │ │ ├── index.js │ │ │ ├── insights.js │ │ │ ├── instantsearch.js │ │ │ ├── middleware.js │ │ │ ├── render-state.js │ │ │ ├── results.js │ │ │ ├── router.js │ │ │ ├── templates.js │ │ │ ├── ui-state.js │ │ │ ├── utils.js │ │ │ ├── widget-factory.js │ │ │ └── widget.js │ │ └── widgets/ │ │ ├── analytics/ │ │ │ └── analytics.js │ │ ├── answers/ │ │ │ ├── answers.js │ │ │ └── defaultTemplates.js │ │ ├── breadcrumb/ │ │ │ ├── breadcrumb.js │ │ │ └── defaultTemplates.js │ │ ├── clear-refinements/ │ │ │ ├── clear-refinements.js │ │ │ └── defaultTemplates.js │ │ ├── configure/ │ │ │ └── configure.js │ │ ├── configure-related-items/ │ │ │ └── configure-related-items.js │ │ ├── current-refinements/ │ │ │ └── current-refinements.js │ │ ├── dynamic-widgets/ │ │ │ └── dynamic-widgets.js │ │ ├── frequently-bought-together/ │ │ │ └── frequently-bought-together.js │ │ ├── geo-search/ │ │ │ ├── GeoSearchRenderer.d.js │ │ │ ├── GeoSearchRenderer.js │ │ │ ├── createHTMLMarker.js │ │ │ ├── defaultTemplates.js │ │ │ └── geo-search.js │ │ ├── hierarchical-menu/ │ │ │ ├── defaultTemplates.js │ │ │ └── hierarchical-menu.js │ │ ├── hits/ │ │ │ ├── defaultTemplates.js │ │ │ └── hits.js │ │ ├── hits-per-page/ │ │ │ └── hits-per-page.js │ │ ├── index/ │ │ │ └── index.js │ │ ├── index.js │ │ ├── infinite-hits/ │ │ │ ├── defaultTemplates.js │ │ │ └── infinite-hits.js │ │ ├── looking-similar/ │ │ │ └── looking-similar.js │ │ ├── menu/ │ │ │ ├── defaultTemplates.js │ │ │ └── menu.js │ │ ├── menu-select/ │ │ │ ├── defaultTemplates.js │ │ │ └── menu-select.js │ │ ├── numeric-menu/ │ │ │ ├── defaultTemplates.js │ │ │ └── numeric-menu.js │ │ ├── pagination/ │ │ │ └── pagination.js │ │ ├── panel/ │ │ │ └── panel.js │ │ ├── places/ │ │ │ └── places.js │ │ ├── powered-by/ │ │ │ └── powered-by.js │ │ ├── query-rule-context/ │ │ │ └── query-rule-context.js │ │ ├── query-rule-custom-data/ │ │ │ └── query-rule-custom-data.js │ │ ├── range-input/ │ │ │ └── range-input.js │ │ ├── range-slider/ │ │ │ └── range-slider.js │ │ ├── rating-menu/ │ │ │ ├── defaultTemplates.js │ │ │ └── rating-menu.js │ │ ├── refinement-list/ │ │ │ ├── defaultTemplates.js │ │ │ └── refinement-list.js │ │ ├── related-products/ │ │ │ └── related-products.js │ │ ├── relevant-sort/ │ │ │ ├── defaultTemplates.js │ │ │ └── relevant-sort.js │ │ ├── search-box/ │ │ │ ├── defaultTemplates.js │ │ │ └── search-box.js │ │ ├── sort-by/ │ │ │ └── sort-by.js │ │ ├── stats/ │ │ │ ├── defaultTemplates.js │ │ │ └── stats.js │ │ ├── toggle-refinement/ │ │ │ ├── defaultTemplates.js │ │ │ └── toggle-refinement.js │ │ ├── trending-items/ │ │ │ └── trending-items.js │ │ └── voice-search/ │ │ ├── defaultTemplates.js │ │ └── voice-search.js │ ├── dist/ │ │ ├── instantsearch.development.d.ts │ │ ├── instantsearch.development.js │ │ ├── instantsearch.production.d.ts │ │ └── instantsearch.production.min.d.ts │ ├── es/ │ │ ├── components/ │ │ │ ├── Answers/ │ │ │ │ ├── Answers.d.ts │ │ │ │ └── Answers.js │ │ │ ├── Breadcrumb/ │ │ │ │ ├── Breadcrumb.d.ts │ │ │ │ └── Breadcrumb.js │ │ │ ├── ClearRefinements/ │ │ │ │ ├── ClearRefinements.d.ts │ │ │ │ └── ClearRefinements.js │ │ │ ├── CurrentRefinements/ │ │ │ │ ├── CurrentRefinements.d.ts │ │ │ │ └── CurrentRefinements.js │ │ │ ├── GeoSearchControls/ │ │ │ │ ├── GeoSearchButton.d.ts │ │ │ │ ├── GeoSearchButton.js │ │ │ │ ├── GeoSearchControls.d.ts │ │ │ │ ├── GeoSearchControls.js │ │ │ │ ├── GeoSearchToggle.d.ts │ │ │ │ └── GeoSearchToggle.js │ │ │ ├── Highlight/ │ │ │ │ ├── Highlight.d.ts │ │ │ │ └── Highlight.js │ │ │ ├── Hits/ │ │ │ │ ├── Hits.d.ts │ │ │ │ └── Hits.js │ │ │ ├── InfiniteHits/ │ │ │ │ ├── InfiniteHits.d.ts │ │ │ │ └── InfiniteHits.js │ │ │ ├── InternalHighlight/ │ │ │ │ ├── InternalHighlight.d.ts │ │ │ │ └── InternalHighlight.js │ │ │ ├── MenuSelect/ │ │ │ │ ├── MenuSelect.d.ts │ │ │ │ └── MenuSelect.js │ │ │ ├── Pagination/ │ │ │ │ ├── Pagination.d.ts │ │ │ │ ├── Pagination.js │ │ │ │ └── PaginationLink.js │ │ │ ├── Panel/ │ │ │ │ ├── Panel.d.ts │ │ │ │ └── Panel.js │ │ │ ├── PoweredBy/ │ │ │ │ ├── PoweredBy.d.ts │ │ │ │ └── PoweredBy.js │ │ │ ├── QueryRuleCustomData/ │ │ │ │ ├── QueryRuleCustomData.d.ts │ │ │ │ └── QueryRuleCustomData.js │ │ │ ├── RangeInput/ │ │ │ │ ├── RangeInput.d.ts │ │ │ │ └── RangeInput.js │ │ │ ├── RefinementList/ │ │ │ │ ├── RefinementList.d.ts │ │ │ │ ├── RefinementList.js │ │ │ │ ├── RefinementListItem.d.ts │ │ │ │ └── RefinementListItem.js │ │ │ ├── RelevantSort/ │ │ │ │ ├── RelevantSort.d.ts │ │ │ │ └── RelevantSort.js │ │ │ ├── ReverseHighlight/ │ │ │ │ ├── ReverseHighlight.d.ts │ │ │ │ └── ReverseHighlight.js │ │ │ ├── ReverseSnippet/ │ │ │ │ ├── ReverseSnippet.d.ts │ │ │ │ └── ReverseSnippet.js │ │ │ ├── SearchBox/ │ │ │ │ ├── SearchBox.d.ts │ │ │ │ └── SearchBox.js │ │ │ ├── Selector/ │ │ │ │ ├── Selector.d.ts │ │ │ │ └── Selector.js │ │ │ ├── Slider/ │ │ │ │ ├── Pit.d.ts │ │ │ │ ├── Pit.js │ │ │ │ ├── Rheostat.d.ts │ │ │ │ ├── Rheostat.js │ │ │ │ ├── Slider.d.ts │ │ │ │ └── Slider.js │ │ │ ├── Snippet/ │ │ │ │ ├── Snippet.d.ts │ │ │ │ └── Snippet.js │ │ │ ├── Stats/ │ │ │ │ ├── Stats.d.ts │ │ │ │ └── Stats.js │ │ │ ├── Template/ │ │ │ │ ├── Template.d.ts │ │ │ │ └── Template.js │ │ │ ├── ToggleRefinement/ │ │ │ │ ├── ToggleRefinement.d.ts │ │ │ │ └── ToggleRefinement.js │ │ │ └── VoiceSearch/ │ │ │ ├── VoiceSearch.d.ts │ │ │ └── VoiceSearch.js │ │ ├── connectors/ │ │ │ ├── answers/ │ │ │ │ ├── connectAnswers.d.ts │ │ │ │ └── connectAnswers.js │ │ │ ├── autocomplete/ │ │ │ │ ├── connectAutocomplete.d.ts │ │ │ │ └── connectAutocomplete.js │ │ │ ├── breadcrumb/ │ │ │ │ ├── connectBreadcrumb.d.ts │ │ │ │ └── connectBreadcrumb.js │ │ │ ├── clear-refinements/ │ │ │ │ ├── connectClearRefinements.d.ts │ │ │ │ └── connectClearRefinements.js │ │ │ ├── configure/ │ │ │ │ ├── connectConfigure.d.ts │ │ │ │ └── connectConfigure.js │ │ │ ├── configure-related-items/ │ │ │ │ ├── connectConfigureRelatedItems.d.ts │ │ │ │ └── connectConfigureRelatedItems.js │ │ │ ├── current-refinements/ │ │ │ │ ├── connectCurrentRefinements.d.ts │ │ │ │ └── connectCurrentRefinements.js │ │ │ ├── dynamic-widgets/ │ │ │ │ ├── connectDynamicWidgets.d.ts │ │ │ │ └── connectDynamicWidgets.js │ │ │ ├── frequently-bought-together/ │ │ │ │ ├── connectFrequentlyBoughtTogether.d.ts │ │ │ │ └── connectFrequentlyBoughtTogether.js │ │ │ ├── geo-search/ │ │ │ │ ├── connectGeoSearch.d.ts │ │ │ │ └── connectGeoSearch.js │ │ │ ├── hierarchical-menu/ │ │ │ │ ├── connectHierarchicalMenu.d.ts │ │ │ │ └── connectHierarchicalMenu.js │ │ │ ├── hits/ │ │ │ │ ├── connectHits.d.ts │ │ │ │ ├── connectHits.js │ │ │ │ ├── connectHitsWithInsights.d.ts │ │ │ │ └── connectHitsWithInsights.js │ │ │ ├── hits-per-page/ │ │ │ │ ├── connectHitsPerPage.d.ts │ │ │ │ └── connectHitsPerPage.js │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ ├── infinite-hits/ │ │ │ │ ├── connectInfiniteHits.d.ts │ │ │ │ ├── connectInfiniteHits.js │ │ │ │ ├── connectInfiniteHitsWithInsights.d.ts │ │ │ │ └── connectInfiniteHitsWithInsights.js │ │ │ ├── looking-similar/ │ │ │ │ ├── connectLookingSimilar.d.ts │ │ │ │ └── connectLookingSimilar.js │ │ │ ├── menu/ │ │ │ │ ├── connectMenu.d.ts │ │ │ │ └── connectMenu.js │ │ │ ├── numeric-menu/ │ │ │ │ ├── connectNumericMenu.d.ts │ │ │ │ └── connectNumericMenu.js │ │ │ ├── pagination/ │ │ │ │ ├── Paginator.d.ts │ │ │ │ ├── Paginator.js │ │ │ │ ├── connectPagination.d.ts │ │ │ │ └── connectPagination.js │ │ │ ├── powered-by/ │ │ │ │ ├── connectPoweredBy.d.ts │ │ │ │ └── connectPoweredBy.js │ │ │ ├── query-rules/ │ │ │ │ ├── connectQueryRules.d.ts │ │ │ │ └── connectQueryRules.js │ │ │ ├── range/ │ │ │ │ ├── connectRange.d.ts │ │ │ │ └── connectRange.js │ │ │ ├── rating-menu/ │ │ │ │ ├── connectRatingMenu.d.ts │ │ │ │ └── connectRatingMenu.js │ │ │ ├── refinement-list/ │ │ │ │ ├── connectRefinementList.d.ts │ │ │ │ └── connectRefinementList.js │ │ │ ├── related-products/ │ │ │ │ ├── connectRelatedProducts.d.ts │ │ │ │ └── connectRelatedProducts.js │ │ │ ├── relevant-sort/ │ │ │ │ ├── connectRelevantSort.d.ts │ │ │ │ └── connectRelevantSort.js │ │ │ ├── search-box/ │ │ │ │ ├── connectSearchBox.d.ts │ │ │ │ └── connectSearchBox.js │ │ │ ├── sort-by/ │ │ │ │ ├── connectSortBy.d.ts │ │ │ │ └── connectSortBy.js │ │ │ ├── stats/ │ │ │ │ ├── connectStats.d.ts │ │ │ │ └── connectStats.js │ │ │ ├── toggle-refinement/ │ │ │ │ ├── connectToggleRefinement.d.ts │ │ │ │ ├── connectToggleRefinement.js │ │ │ │ ├── types.d.ts │ │ │ │ └── types.js │ │ │ ├── trending-items/ │ │ │ │ ├── connectTrendingItems.d.ts │ │ │ │ └── connectTrendingItems.js │ │ │ └── voice-search/ │ │ │ ├── connectVoiceSearch.d.ts │ │ │ └── connectVoiceSearch.js │ │ ├── helpers/ │ │ │ ├── components/ │ │ │ │ ├── Highlight.d.ts │ │ │ │ ├── Highlight.js │ │ │ │ ├── ReverseHighlight.d.ts │ │ │ │ ├── ReverseHighlight.js │ │ │ │ ├── ReverseSnippet.d.ts │ │ │ │ ├── ReverseSnippet.js │ │ │ │ ├── Snippet.d.ts │ │ │ │ ├── Snippet.js │ │ │ │ ├── index.d.ts │ │ │ │ └── index.js │ │ │ ├── get-insights-anonymous-user-token.d.ts │ │ │ ├── get-insights-anonymous-user-token.js │ │ │ ├── highlight.d.ts │ │ │ ├── highlight.js │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ ├── insights.d.ts │ │ │ ├── insights.js │ │ │ ├── reverseHighlight.d.ts │ │ │ ├── reverseHighlight.js │ │ │ ├── reverseSnippet.d.ts │ │ │ ├── reverseSnippet.js │ │ │ ├── snippet.d.ts │ │ │ └── snippet.js │ │ ├── index.d.ts │ │ ├── index.js │ │ ├── lib/ │ │ │ ├── InstantSearch.d.ts │ │ │ ├── InstantSearch.js │ │ │ ├── createHelpers.d.ts │ │ │ ├── createHelpers.js │ │ │ ├── formatNumber.d.ts │ │ │ ├── formatNumber.js │ │ │ ├── infiniteHitsCache/ │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.js │ │ │ │ ├── sessionStorage.d.ts │ │ │ │ └── sessionStorage.js │ │ │ ├── insights/ │ │ │ │ ├── client.d.ts │ │ │ │ ├── client.js │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.js │ │ │ │ ├── listener.d.ts │ │ │ │ └── listener.js │ │ │ ├── main.js │ │ │ ├── routers/ │ │ │ │ ├── history.d.ts │ │ │ │ ├── history.js │ │ │ │ ├── index.d.ts │ │ │ │ └── index.js │ │ │ ├── server.d.ts │ │ │ ├── server.js │ │ │ ├── stateMappings/ │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.js │ │ │ │ ├── simple.d.ts │ │ │ │ ├── simple.js │ │ │ │ ├── singleIndex.d.ts │ │ │ │ └── singleIndex.js │ │ │ ├── suit.d.ts │ │ │ ├── suit.js │ │ │ ├── templating/ │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.js │ │ │ │ ├── prepareTemplateProps.d.ts │ │ │ │ ├── prepareTemplateProps.js │ │ │ │ ├── renderTemplate.d.ts │ │ │ │ └── renderTemplate.js │ │ │ ├── utils/ │ │ │ │ ├── addWidgetId.d.ts │ │ │ │ ├── addWidgetId.js │ │ │ │ ├── capitalize.d.ts │ │ │ │ ├── capitalize.js │ │ │ │ ├── checkIndexUiState.d.ts │ │ │ │ ├── checkIndexUiState.js │ │ │ │ ├── checkRendering.d.ts │ │ │ │ ├── checkRendering.js │ │ │ │ ├── clearRefinements.d.ts │ │ │ │ ├── clearRefinements.js │ │ │ │ ├── concatHighlightedParts.d.ts │ │ │ │ ├── concatHighlightedParts.js │ │ │ │ ├── convertNumericRefinementsToFilters.d.ts │ │ │ │ ├── convertNumericRefinementsToFilters.js │ │ │ │ ├── createConcurrentSafePromise.d.ts │ │ │ │ ├── createConcurrentSafePromise.js │ │ │ │ ├── createSendEventForFacet.d.ts │ │ │ │ ├── createSendEventForFacet.js │ │ │ │ ├── createSendEventForHits.d.ts │ │ │ │ ├── createSendEventForHits.js │ │ │ │ ├── cx.d.ts │ │ │ │ ├── cx.js │ │ │ │ ├── debounce.d.ts │ │ │ │ ├── debounce.js │ │ │ │ ├── defer.d.ts │ │ │ │ ├── defer.js │ │ │ │ ├── detect-insights-client.d.ts │ │ │ │ ├── detect-insights-client.js │ │ │ │ ├── documentation.d.ts │ │ │ │ ├── documentation.js │ │ │ │ ├── escape-highlight.d.ts │ │ │ │ ├── escape-highlight.js │ │ │ │ ├── escape-html.d.ts │ │ │ │ ├── escape-html.js │ │ │ │ ├── escape.d.ts │ │ │ │ ├── escape.js │ │ │ │ ├── escapeFacetValue.d.ts │ │ │ │ ├── escapeFacetValue.js │ │ │ │ ├── escapeRefinement.js │ │ │ │ ├── find.d.ts │ │ │ │ ├── find.js │ │ │ │ ├── findIndex.d.ts │ │ │ │ ├── findIndex.js │ │ │ │ ├── geo-search.d.ts │ │ │ │ ├── geo-search.js │ │ │ │ ├── getAppIdAndApiKey.d.ts │ │ │ │ ├── getAppIdAndApiKey.js │ │ │ │ ├── getContainerNode.d.ts │ │ │ │ ├── getContainerNode.js │ │ │ │ ├── getHighlightFromSiblings.d.ts │ │ │ │ ├── getHighlightFromSiblings.js │ │ │ │ ├── getHighlightedParts.d.ts │ │ │ │ ├── getHighlightedParts.js │ │ │ │ ├── getObjectType.d.ts │ │ │ │ ├── getObjectType.js │ │ │ │ ├── getPropertyByPath.d.ts │ │ │ │ ├── getPropertyByPath.js │ │ │ │ ├── getRefinements.d.ts │ │ │ │ ├── getRefinements.js │ │ │ │ ├── getWidgetAttribute.d.ts │ │ │ │ ├── getWidgetAttribute.js │ │ │ │ ├── hits-absolute-position.d.ts │ │ │ │ ├── hits-absolute-position.js │ │ │ │ ├── hits-query-id.d.ts │ │ │ │ ├── hits-query-id.js │ │ │ │ ├── hydrateRecommendCache.d.ts │ │ │ │ ├── hydrateRecommendCache.js │ │ │ │ ├── hydrateSearchClient.d.ts │ │ │ │ ├── hydrateSearchClient.js │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.js │ │ │ │ ├── isDomElement.d.ts │ │ │ │ ├── isDomElement.js │ │ │ │ ├── isEqual.d.ts │ │ │ │ ├── isEqual.js │ │ │ │ ├── isFacetRefined.d.ts │ │ │ │ ├── isFacetRefined.js │ │ │ │ ├── isFiniteNumber.d.ts │ │ │ │ ├── isFiniteNumber.js │ │ │ │ ├── isIndexWidget.d.ts │ │ │ │ ├── isIndexWidget.js │ │ │ │ ├── isPlainObject.d.ts │ │ │ │ ├── isPlainObject.js │ │ │ │ ├── isSpecialClick.d.ts │ │ │ │ ├── isSpecialClick.js │ │ │ │ ├── logger.d.ts │ │ │ │ ├── logger.js │ │ │ │ ├── mergeSearchParameters.d.ts │ │ │ │ ├── mergeSearchParameters.js │ │ │ │ ├── noop.d.ts │ │ │ │ ├── noop.js │ │ │ │ ├── omit.d.ts │ │ │ │ ├── omit.js │ │ │ │ ├── prepareTemplateProps.d.ts │ │ │ │ ├── prepareTemplateProps.js │ │ │ │ ├── range.d.ts │ │ │ │ ├── range.js │ │ │ │ ├── render-args.d.ts │ │ │ │ ├── render-args.js │ │ │ │ ├── renderTemplate.d.ts │ │ │ │ ├── renderTemplate.js │ │ │ │ ├── resolveSearchParameters.d.ts │ │ │ │ ├── resolveSearchParameters.js │ │ │ │ ├── reverseHighlightedParts.d.ts │ │ │ │ ├── reverseHighlightedParts.js │ │ │ │ ├── safelyRunOnBrowser.d.ts │ │ │ │ ├── safelyRunOnBrowser.js │ │ │ │ ├── serializer.d.ts │ │ │ │ ├── serializer.js │ │ │ │ ├── setIndexHelperState.d.ts │ │ │ │ ├── setIndexHelperState.js │ │ │ │ ├── toArray.d.ts │ │ │ │ ├── toArray.js │ │ │ │ ├── typedObject.d.ts │ │ │ │ ├── typedObject.js │ │ │ │ ├── unescape.d.ts │ │ │ │ ├── unescape.js │ │ │ │ ├── unescapeRefinement.js │ │ │ │ ├── uniq.d.ts │ │ │ │ ├── uniq.js │ │ │ │ ├── uuid.d.ts │ │ │ │ ├── uuid.js │ │ │ │ ├── walkIndex.d.ts │ │ │ │ └── walkIndex.js │ │ │ ├── version.d.ts │ │ │ ├── version.js │ │ │ └── voiceSearchHelper/ │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ ├── types.d.ts │ │ │ └── types.js │ │ ├── middlewares/ │ │ │ ├── createInsightsMiddleware.d.ts │ │ │ ├── createInsightsMiddleware.js │ │ │ ├── createMetadataMiddleware.d.ts │ │ │ ├── createMetadataMiddleware.js │ │ │ ├── createRouterMiddleware.d.ts │ │ │ ├── createRouterMiddleware.js │ │ │ ├── index.d.ts │ │ │ └── index.js │ │ ├── package.json │ │ ├── templates/ │ │ │ ├── carousel/ │ │ │ │ ├── carousel.d.ts │ │ │ │ └── carousel.js │ │ │ ├── index.d.ts │ │ │ └── index.js │ │ ├── types/ │ │ │ ├── algoliasearch.d.ts │ │ │ ├── algoliasearch.js │ │ │ ├── component.d.ts │ │ │ ├── component.js │ │ │ ├── connector.d.ts │ │ │ ├── connector.js │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ ├── insights.d.ts │ │ │ ├── insights.js │ │ │ ├── instantsearch.d.ts │ │ │ ├── instantsearch.js │ │ │ ├── middleware.d.ts │ │ │ ├── middleware.js │ │ │ ├── render-state.d.ts │ │ │ ├── render-state.js │ │ │ ├── results.d.ts │ │ │ ├── results.js │ │ │ ├── router.d.ts │ │ │ ├── router.js │ │ │ ├── templates.d.ts │ │ │ ├── templates.js │ │ │ ├── ui-state.d.ts │ │ │ ├── ui-state.js │ │ │ ├── utils.d.ts │ │ │ ├── utils.js │ │ │ ├── widget-factory.d.ts │ │ │ ├── widget-factory.js │ │ │ ├── widget.d.ts │ │ │ └── widget.js │ │ └── widgets/ │ │ ├── analytics/ │ │ │ ├── analytics.d.ts │ │ │ └── analytics.js │ │ ├── answers/ │ │ │ ├── answers.d.ts │ │ │ ├── answers.js │ │ │ ├── defaultTemplates.d.ts │ │ │ └── defaultTemplates.js │ │ ├── breadcrumb/ │ │ │ ├── breadcrumb.d.ts │ │ │ ├── breadcrumb.js │ │ │ ├── defaultTemplates.d.ts │ │ │ └── defaultTemplates.js │ │ ├── clear-refinements/ │ │ │ ├── clear-refinements.d.ts │ │ │ ├── clear-refinements.js │ │ │ ├── defaultTemplates.d.ts │ │ │ └── defaultTemplates.js │ │ ├── configure/ │ │ │ ├── configure.d.ts │ │ │ └── configure.js │ │ ├── configure-related-items/ │ │ │ ├── configure-related-items.d.ts │ │ │ └── configure-related-items.js │ │ ├── current-refinements/ │ │ │ ├── current-refinements.d.ts │ │ │ └── current-refinements.js │ │ ├── dynamic-widgets/ │ │ │ ├── dynamic-widgets.d.ts │ │ │ └── dynamic-widgets.js │ │ ├── frequently-bought-together/ │ │ │ ├── frequently-bought-together.d.ts │ │ │ └── frequently-bought-together.js │ │ ├── geo-search/ │ │ │ ├── GeoSearchRenderer.d.js │ │ │ ├── GeoSearchRenderer.d.ts │ │ │ ├── GeoSearchRenderer.js │ │ │ ├── createHTMLMarker.d.ts │ │ │ ├── createHTMLMarker.js │ │ │ ├── defaultTemplates.d.ts │ │ │ ├── defaultTemplates.js │ │ │ ├── geo-search.d.ts │ │ │ └── geo-search.js │ │ ├── hierarchical-menu/ │ │ │ ├── defaultTemplates.d.ts │ │ │ ├── defaultTemplates.js │ │ │ ├── hierarchical-menu.d.ts │ │ │ └── hierarchical-menu.js │ │ ├── hits/ │ │ │ ├── defaultTemplates.d.ts │ │ │ ├── defaultTemplates.js │ │ │ ├── hits.d.ts │ │ │ └── hits.js │ │ ├── hits-per-page/ │ │ │ ├── hits-per-page.d.ts │ │ │ └── hits-per-page.js │ │ ├── index/ │ │ │ ├── index.d.ts │ │ │ └── index.js │ │ ├── index.d.ts │ │ ├── index.js │ │ ├── infinite-hits/ │ │ │ ├── defaultTemplates.d.ts │ │ │ ├── defaultTemplates.js │ │ │ ├── infinite-hits.d.ts │ │ │ └── infinite-hits.js │ │ ├── looking-similar/ │ │ │ ├── looking-similar.d.ts │ │ │ └── looking-similar.js │ │ ├── menu/ │ │ │ ├── defaultTemplates.d.ts │ │ │ ├── defaultTemplates.js │ │ │ ├── menu.d.ts │ │ │ └── menu.js │ │ ├── menu-select/ │ │ │ ├── defaultTemplates.d.ts │ │ │ ├── defaultTemplates.js │ │ │ ├── menu-select.d.ts │ │ │ └── menu-select.js │ │ ├── numeric-menu/ │ │ │ ├── defaultTemplates.d.ts │ │ │ ├── defaultTemplates.js │ │ │ ├── numeric-menu.d.ts │ │ │ └── numeric-menu.js │ │ ├── pagination/ │ │ │ ├── pagination.d.ts │ │ │ └── pagination.js │ │ ├── panel/ │ │ │ ├── panel.d.ts │ │ │ └── panel.js │ │ ├── places/ │ │ │ ├── places.d.ts │ │ │ └── places.js │ │ ├── powered-by/ │ │ │ ├── powered-by.d.ts │ │ │ └── powered-by.js │ │ ├── query-rule-context/ │ │ │ ├── query-rule-context.d.ts │ │ │ └── query-rule-context.js │ │ ├── query-rule-custom-data/ │ │ │ ├── query-rule-custom-data.d.ts │ │ │ └── query-rule-custom-data.js │ │ ├── range-input/ │ │ │ ├── range-input.d.ts │ │ │ └── range-input.js │ │ ├── range-slider/ │ │ │ ├── range-slider.d.ts │ │ │ └── range-slider.js │ │ ├── rating-menu/ │ │ │ ├── defaultTemplates.d.ts │ │ │ ├── defaultTemplates.js │ │ │ ├── rating-menu.d.ts │ │ │ └── rating-menu.js │ │ ├── refinement-list/ │ │ │ ├── defaultTemplates.d.ts │ │ │ ├── defaultTemplates.js │ │ │ ├── refinement-list.d.ts │ │ │ └── refinement-list.js │ │ ├── related-products/ │ │ │ ├── related-products.d.ts │ │ │ └── related-products.js │ │ ├── relevant-sort/ │ │ │ ├── defaultTemplates.d.ts │ │ │ ├── defaultTemplates.js │ │ │ ├── relevant-sort.d.ts │ │ │ └── relevant-sort.js │ │ ├── search-box/ │ │ │ ├── defaultTemplates.d.ts │ │ │ ├── defaultTemplates.js │ │ │ ├── search-box.d.ts │ │ │ └── search-box.js │ │ ├── sort-by/ │ │ │ ├── sort-by.d.ts │ │ │ └── sort-by.js │ │ ├── stats/ │ │ │ ├── defaultTemplates.d.ts │ │ │ ├── defaultTemplates.js │ │ │ ├── stats.d.ts │ │ │ └── stats.js │ │ ├── toggle-refinement/ │ │ │ ├── defaultTemplates.d.ts │ │ │ ├── defaultTemplates.js │ │ │ ├── toggle-refinement.d.ts │ │ │ └── toggle-refinement.js │ │ ├── trending-items/ │ │ │ ├── trending-items.d.ts │ │ │ └── trending-items.js │ │ └── voice-search/ │ │ ├── defaultTemplates.d.ts │ │ ├── defaultTemplates.js │ │ ├── voice-search.d.ts │ │ └── voice-search.js │ ├── package.json │ └── scripts/ │ └── transforms/ │ ├── README.md │ ├── __testfixtures__/ │ │ └── addWidget-to-addWidgets/ │ │ ├── global.input.js │ │ ├── global.output.js │ │ ├── imported.input.js │ │ ├── imported.output.js │ │ ├── mixed.input.js │ │ ├── mixed.output.js │ │ ├── remove.input.js │ │ └── remove.output.js │ ├── __tests__/ │ │ └── addWidget-to-addWidgets.test.js │ └── addWidget-to-addWidgets.js ├── languages/ │ ├── index.php │ ├── wp-search-with-algolia-it_IT.mo │ ├── wp-search-with-algolia-it_IT.po │ └── wp-search-with-algolia.pot ├── package.json ├── phpcs.xml ├── templates/ │ ├── autocomplete.php │ ├── instantsearch-modern.php │ └── instantsearch.php ├── uninstall.php ├── vendor_prefixed/ │ ├── .gitignore │ ├── algolia/ │ │ └── algoliasearch-client-php/ │ │ ├── LICENSE │ │ └── src/ │ │ ├── AccountClient.php │ │ ├── Algolia.php │ │ ├── AnalyticsClient.php │ │ ├── Cache/ │ │ │ ├── FileCacheDriver.php │ │ │ └── NullCacheDriver.php │ │ ├── Config/ │ │ │ ├── AbstractConfig.php │ │ │ ├── AnalyticsConfig.php │ │ │ ├── InsightsConfig.php │ │ │ ├── PersonalizationConfig.php │ │ │ ├── PlacesConfig.php │ │ │ ├── RecommendConfig.php │ │ │ ├── RecommendationConfig.php │ │ │ └── SearchConfig.php │ │ ├── Exceptions/ │ │ │ ├── AlgoliaException.php │ │ │ ├── BadRequestException.php │ │ │ ├── CannotWaitException.php │ │ │ ├── MissingObjectId.php │ │ │ ├── NotFoundException.php │ │ │ ├── ObjectNotFoundException.php │ │ │ ├── RequestException.php │ │ │ ├── RetriableException.php │ │ │ ├── UnreachableException.php │ │ │ └── ValidUntilNotFoundException.php │ │ ├── Http/ │ │ │ ├── CurlHttpClient.php │ │ │ ├── GuzzleHttpClient.php │ │ │ ├── HttpClientInterface.php │ │ │ └── Psr7/ │ │ │ ├── BufferStream.php │ │ │ ├── PumpStream.php │ │ │ ├── Request.php │ │ │ ├── Response.php │ │ │ ├── Stream.php │ │ │ ├── Uri.php │ │ │ ├── UriResolver.php │ │ │ └── functions.php │ │ ├── Insights/ │ │ │ └── UserInsightsClient.php │ │ ├── InsightsClient.php │ │ ├── Iterators/ │ │ │ ├── AbstractAlgoliaIterator.php │ │ │ ├── ObjectIterator.php │ │ │ ├── RuleIterator.php │ │ │ └── SynonymIterator.php │ │ ├── Log/ │ │ │ └── DebugLogger.php │ │ ├── PersonalizationClient.php │ │ ├── PlacesClient.php │ │ ├── RecommendClient.php │ │ ├── RecommendationClient.php │ │ ├── RequestOptions/ │ │ │ ├── RequestOptions.php │ │ │ └── RequestOptionsFactory.php │ │ ├── Response/ │ │ │ ├── AbstractResponse.php │ │ │ ├── AddApiKeyResponse.php │ │ │ ├── BatchIndexingResponse.php │ │ │ ├── DeleteApiKeyResponse.php │ │ │ ├── DictionaryResponse.php │ │ │ ├── IndexingResponse.php │ │ │ ├── MultiResponse.php │ │ │ ├── MultipleIndexBatchIndexingResponse.php │ │ │ ├── NullResponse.php │ │ │ ├── RestoreApiKeyResponse.php │ │ │ └── UpdateApiKeyResponse.php │ │ ├── RetryStrategy/ │ │ │ ├── ApiWrapper.php │ │ │ ├── ApiWrapperInterface.php │ │ │ ├── ClusterHosts.php │ │ │ ├── Host.php │ │ │ └── HostCollection.php │ │ ├── SearchClient.php │ │ ├── SearchIndex.php │ │ ├── Support/ │ │ │ ├── Helpers.php │ │ │ └── UserAgent.php │ │ └── functions.php │ ├── autoload-classmap.php │ ├── autoload-files.php │ ├── autoload.php │ └── psr/ │ ├── http-message/ │ │ ├── LICENSE │ │ └── src/ │ │ ├── MessageInterface.php │ │ ├── RequestInterface.php │ │ ├── ResponseInterface.php │ │ ├── ServerRequestInterface.php │ │ ├── StreamInterface.php │ │ ├── UploadedFileInterface.php │ │ └── UriInterface.php │ ├── log/ │ │ ├── LICENSE │ │ └── Psr/ │ │ └── Log/ │ │ ├── AbstractLogger.php │ │ ├── InvalidArgumentException.php │ │ ├── LogLevel.php │ │ ├── LoggerAwareInterface.php │ │ ├── LoggerAwareTrait.php │ │ ├── LoggerInterface.php │ │ ├── LoggerTrait.php │ │ ├── NullLogger.php │ │ └── Test/ │ │ ├── DummyTest.php │ │ ├── LoggerInterfaceTest.php │ │ └── TestLogger.php │ └── simple-cache/ │ ├── LICENSE.md │ └── src/ │ ├── CacheException.php │ ├── CacheInterface.php │ └── InvalidArgumentException.php └── wordfence-vendor.txt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ # A set of files you probably don't want in your WordPress.org distribution .babelrc export-ignore .circleci/config.yml export-ignore .deployignore export-ignore .distignore export-ignore .DS_Store export-ignore .editorconfig export-ignore .eslintignore export-ignore .eslintrc export-ignore .git export-ignore .gitattributes export-ignore .github export-ignore .gitignore export-ignore .gitlab-ci.yml export-ignore .phpcs.xml export-ignore .phpcs.xml.dist export-ignore .travis.yml export-ignore *.sql export-ignore *.tar.gz export-ignore *.zip export-ignore behat.yml export-ignore bin export-ignore bitbucket-pipelines.yml export-ignore composer.json export-ignore composer.lock export-ignore dependencies.yml export-ignore Gruntfile.js export-ignore multisite.xml export-ignore multisite.xml.dist export-ignore node_modules export-ignore package-lock.json export-ignore package.json export-ignore phpcs.xml export-ignore phpcs.xml.dist export-ignore phpunit.xml export-ignore phpunit.xml.dist export-ignore README.md export-ignore tests export-ignore Thumbs.db export-ignore webpack.config.js export-ignore wp-cli.local.yml export-ignore yarn.lock export-ignore /assets export-ignore /vendor export-ignore ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/LICENSE.md ================================================ GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. ================================================ FILE: .github/MIT-LICENSE.md ================================================ The MIT License (MIT) Copyright (c) 2016 Algolia http://www.algolia.com/ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: .gitignore ================================================ *.sql *.tar.gz *.zip .DS_Store .editorconfig Thumbs.db bin public node_modules tests tmp /vendor ================================================ FILE: .nvmrc ================================================ lts/erbium ================================================ FILE: CHANGELOG.md ================================================ ## 2.11.3 * Fixed: minimum requirement discrepancies in readme and defined constant. * Updated: cleaned out some unused CSS selectors from really old functionality. * Updated: make use of `.card` class from WP core for more consistant styling. * Updated: Admin notice around term updating when term is assigned to many posts. ## 2.11.2 * Fixed: Valid Search key checks for new applications. ## 2.11.1 * Fixed: Fatal error if not able to retreive searchable posts index object. * Fixed: Fatal error potential for non-set debounce array index. ## 2.11.0 * Updated: Algolia PHP client (addresses PHP 8.4 compatibility notices) * Updated: UI wording to match Algolia references and Instantsearch notes. * Fixed: Return JSON error instead of echo exception message and continue to throw exception. * Added: Inline documentation for various custom filters and actions. * Added: Output custom debounce values in Autocomplete settings UI. ## 2.10.4 * Fixed: revised asset loading for InstantSearch with Full Site Editing enabled. * Added: Striping styles for Autocomplete table list. ## 2.10.3 * Added: Filter to still output frontend config data when using Instantsearch and FSE Theme. * Added: Filtering of new filter to attempt auto-detection of a FSE theme being in use. ## 2.10.2 * Added: Checkbox option to enable insights in Instantsearch templates. Please review changes to those. ## 2.10.1 * Fixed: Issues around admin notifications on the Autocomplete settings page, introduced in 2.10.0 ## 2.10.0 * Added an option to set the debounce timeout value which applies to all indexes by default, but can be customized for each index with a filter: Dynamic filter name: `algolia_autocomplete_debounce_{$index_name}_{$index_type}` Where `$index_name` is defined by the Index name prefix set on the WP Search with Algolia settings page and `$index_type` is the type of index. Assuming `wp_` is the Index name prefix, the debounce timeout filters would be: ``` algolia_autocomplete_debounce_wp_searchable_posts algolia_autocomplete_debounce_wp_post algolia_autocomplete_debounce_wp_page algolia_autocomplete_debounce_wp_my_custom_post_type algolia_autocomplete_debounce_wp_users algolia_autocomplete_debounce_wp_terms_category algolia_autocomplete_debounce_wp_terms_post_tag algolia_autocomplete_debounce_wp_terms_my_custom_taxonomy ``` Note that the Algolia Autocomplete settings must be saved after creating one of the above filters. ## 2.9.0 * Added: Instantsearch Template options. Choose between “Legacy” hogan.js/WP Utils templates and “Modern” Javascript string literals. “Modern” is more in line with Algolia Documentation. * Added: ability to customize default Headers for Algolia Search Client configuration. * Added: Initial support for programmatic Secured API key creation. * Updated: Sync’d up get_items() methods to allow for specifying specific IDs for posts, terms, and users. * Updated: Instantsearch templates use “Posts per page” amount by default, from WordPress Reading settings. * Updated: Amended Autocomplete settings page to remove more builtin post types that don’t need to be available. ## 2.8.3 * Fixed: "Function _load_textdomain_just_in_time was called incorrectly" notices. ## 2.8.2 * Updated: Wording and UI details around the settings pages for better and more accurate reflection. * Updated: Confirmed compatibility with WP 6.7.x * Added: New page regarding Premium support from WebDevStudios. Let's work together. ## 2.8.1 * Updated: WP Search with Algolia Pro features list for version 1.4.0 ## 2.8.0 * Added: Filter to customize Algolia SearchClient configuration with connect/read/write timeouts. * Updated: Prevent table content from being concatenated. Thanks @rodrigo-arias * Updated: Pass `$post_id` to `algolia_get_post_images` filter. * Updated: Confirmed compatibility with WP 6.5 ## 2.7.1 * Fixed: Instantsearch.php template file. "Powered By Algolia" Instantsearch widget wrapped in a check for if the "Remove powered by" option is checked. This should match already working behavior with Autocomplete dropdown. ## 2.7.0 * Updated: Moved post sync action from `save_post` to `wp_after_insert_post`. This allows for the sync to wait until after post meta and terms have been updated. Removes need to click save twice to sync everything. * Updated: WP Search with Algolia feature list. ## 2.6.2 * Fixed: More performance updates and resolutions around WP All Import. ## 2.6.1 * Fixed: Performance issues related to delete operations. * Fixed: Performance issues around WP All Import. ## 2.6.0 * Added: Support for syncing imported items when "fast mode" from WP All Import enabled. * Added: Support for updating child posts if parent post's slug has been updated. * Added: Support for updating posts when an associated term has been updatd. * Added: Wait for delete operations to complete before moving to updates. * Updated: Algolia Search library to 4.18.x * Updated: InstantSearch library to 4.56.x ## 2.5.4 * Updated: Ensure reindexing completes when using the from_batch flag with CLI. * Updated: Assigned Algolia_Admin instance to a property for access elsewhere. ## 2.5.3 * Updated: Autocomplete template file with user link fix when cmd/ctrl clicking. * Updated: Class method visibility from protected to public. ## 2.5.2 * Updated: Fixed hits per page configuration for instantsearch * Added: Custom hook for settings page override. ## 2.5.1 * Updated readme.txt with more plugin information. * Repositioned help info on settings screens. ## 2.5.0 * Introduction of WP Search with Algolia Pro availability. * Added `algolia_custom_template_location` filter to allow specifying custom template locations besides just your active theme. * Templates: added action hooks at the end of Autocomplete and Instantsearch hit template blocks. * Updated `algolia_changes_watchers` filter to also receive the current indices. * Added watcher support for term and user meta updates. * Updated bundled CSS to better match selectors for default used widgets in the templates. * Clarified some details around Autocomplete settings and what can be done in each setting state. * Updated admin menu icon to use Algolia logo when no settings configured. ## 2.4.0 * Increase minimum PHP version to PHP 7.4 * Fixed PHP8 compatibility issues * Prefixed Algolia library to avoid potential conflicts with other code using the same libraries. * Revised copy and wording around the plugin for better clarity. * Deprecate the `algolia_should_require_search_client` filter in favor of prefixed Algolia PHP Client namespace ## 2.3.1 * Update autocomplete template to use addEventListener instead of onload function * Update Algolia InstantSearch.js to 4.49.1 ## 2.3.0 * Add algolia_should_override_autocomplete filter to override enable/disable status of Autocomplete * Add from_batch argument to the re-index WP-CLI command * Update excluded custom post types and taxonomies to include Core WordPress' internal CPTs and taxonomies * Update Algolia logos to match the latest version * Remove jQuery usage and dependency from templates * Update Algolia JavaScript API Client to 4.14.2 * Update Algolia InstantSearch.js to 4.49.0 * Update Algolia PHP API Client to 3.3.2 ## 2.2.0 * Add alert to Push Settings button on the Search Page. * Replace attributesToIndex index setting with searchableAttributes. * Replace outdated Instant Search widget class. * Improve drag and drop column description text on the Autocomplete page. * Remove inline CSS for Max. Suggestions input. * Update Algolia JavaScript API Client to 4.13.0 * Update Algolia InstantSearch.js to 4.40.5 * Update Algolia Autocomplete.js to 0.38.1 * Update Algolia PHP API Client to 3.2.0 ## 2.1.0 * Add algolia_update_records filter to allow inspection and filtering records during update operation. * Add algolia_re_index_records filter to allow inspection and filtering records during re-index operation. * Catch some Aloglia PHP Client exceptions that were previously uncaught during record updating and re-indexing. * Fix an issue where SearchIndex::saveObjects was called twice during re-index operations. * Update Algolia PHP API Client to 3.1.0 ## 2.0.1 * Fix for users that enable intstantsearch but not autocomplete by adding algoliasearch client as direct dependency of both ## 2.0.0 * Breaking changes for users with customized autocomplete.php / instantsearch.php template in their theme. * Update autocomplete.php and instantsearch.php templates for compatibility with new JS libs. * Update Algolia JavaScript API Client to 4.10.3 * Update Algolia InstantSearch.js to 4.25.2 * Update Algolia Autocomplete.js to 0.38.0 * Update Algolia PHP API Client to 3.0.2 ## 1.8.0 * Focus on template versioning and update messaging * Add Algolia_Template_Utils class * Deprecate Algolia_Template_Loader::locate_template method * Deprecate Algolia_Plugin::get_templates_path method * Deprecate algolia_templates_path filter * Add Algolia_Update_Messages class * Add Algolia_Admin_Template_Notices class * Add Algolia_Version_Utils class ## 1.7.0 * Remove 'screen' media attribute from enqueued CSS * Update Algolia PHP Search Client to version 2.7.3. * Add "exclude" methods and filters * Deprecate "blacklist" methods and filters * Fix replica RequestOptions error * Fix PHP 8 usort deprecation warning * Fix JQMIGRATE event shorthand is deprecated warnings in instantsearch.php and autocomplete.php templates * Add "@version" to template file headers ## 1.6.0 * Fix deletion of post records created before indexing was enabled * Update Algolia PHP Search Client to version 2.7.1. * Add Algolia_Plugin_Factory to create and return a shared Algolia_Plugin instance * Add Algolia_Search_Client_Factory to return a new Algolia\AlgoliaSearch\SearchClient instance * Add Algolia_Http_Client_Interface_Factory to create and return a shared Php53HttpClient instance * Add algolia_php_53_http_client_options filter to supply cURL options to Php53HttpClient instance * Deprecate Algolia_Plugin:get_instance() which will be removed in an upcoming release ## 1.5.0 * Fix an issue where Pinterest follows a link to the Algolia domain to source text and/or images * Move Algolia scripts to footer by default * Changes algolia_load_scripts_in_footer filter default argument to "true" * Move autocomplete.php template output to footer by default ## 1.4.0 * Update Algolia PHP Search Client version 2.7.0. * Update Algolia JS libraries to most recent compatible (non-breaking) versions * Updates autocomplete.js to 0.37.1 (current release as of 2020-01-27) * Updates algoliasearch to 3.35.1 (last of the 3.x series) * Updates instantsearch.js to 1.12.1 (last of the 1.x series) ## 1.3.0 * Fix an issue where, under some circumstances, when a post with a featured image was deleted, the post might be accidentally re-indexed * Fix bug that prevented reindex display notices * Add algolia_load_scripts_in_footer filter to allow enqueueing the scripts in the footer instead of in the head * Add new filters for multisite developers ## 1.2.0 * Use filtered value of 'hitsPerPage' as 'posts_per_page' query param * Fix broken SVG * Add highlighting to backend search results - props @philipnewcomer ## 1.1.0 * Minimum PHP version requirement is now PHP 7.2 * Minimum WordPress version requirement is now WP 5.0 * Internationalization/localization improvements, textdomain matches plugin slug * Addressed a potential WSOD if minimum PHP and WP version requirements were not met * Tested on WP 5.3 ## 1.0.0 * Initial release. ================================================ FILE: README.txt ================================================ === WP Search with Algolia === Contributors: WebDevStudios, williamsba1, tw2113, mrasharirfan, scottbasgaard, gregrickaby, richaber, daveromsey Tags: algolia, autocomplete, instantsearch, relevance search, ai search Requires at least: 6.7.2 Tested up to: 6.9 Requires PHP: 7.4 Stable tag: 2.11.3 License: GNU General Public License v2.0, MIT License Use the power of Algolia AI Search & Discovery to enhance your website's search. Enable AI-powered Autocomplete and InstantSearch for fast, accurate results and relevance. == Description == Easily integrate the powerful search tool Algolia AI Search & Discovery directly into your WordPress website. Quickly index all of your website’s content and provide lightning fast and accurate search results within minutes! Built and supported by WebDevStudios, the website agency behind Custom Post Type UI, WP Search with Algolia immediately improves search on your website. Your users will be impressed! Enable Autocomplete and Instantsearch to immediately provide a more robust search experience to your visitors. Plus, you receive full control over the look, feel, and relevance of your users' search experience. = Features = * One-click indexing of all content in WordPress * Relevant, faceted ready search results in milliseconds with native typo-tolerance from Algolia AI Search & Discovery * Super granular control on search content relevancy and content positioning * Language-agnostic * WordPress hooks and filters available for easy customization of indexed content. This plugin requires API keys from [Algolia](https://www.algolia.com/). API keys are free for small personal projects and non-commercial use. Learn more about [commercial use pricing](https://www.algolia.com/pricing/). === WP Search with Algolia Pro === Introducing **WP Search with Algolia Pro**, our new premium version of WP Search with Algolia! Pro features include: * Multisite Network-wide support! * Create a global search for content across all the sites in your network all in one Algolia index. * WooCommerce support * Indexing Product data including SKU, pricing (standard and variable), short descriptions, dimensions, and more. * Total sales and total ratings indexed for popularity * Advanced SEO support with Yoast SEO, All in One SEO, Rank Math SEO, SEOPress, and The SEO Framework. * Content level settings to exclude individual content from the search index * Set Algolia’s indexing to match with existing search engine “noindex” settings Are you ready to go Pro? Check out [WP Search with Algolia Pro on Pluginize](https://pluginize.com/plugins/wp-search-with-algolia-pro/)! = Links = * [WebDevStudios](https://webdevstudios.com) * [Algolia](https://algolia.com) * [Documentation](https://github.com/WebDevStudios/wp-search-with-algolia/wiki) * [Support](https://wordpress.org/support/plugin/wp-search-with-algolia/) * [Feature requests and bugs](https://github.com/WebDevStudios/wp-search-with-algolia/issues) * [WP Search with Algolia Snippet Library](https://github.com/WebDevStudios/algolia-snippet-library) *This plugin is a derivative work of the code from the [Search by Algolia – Instant & Relevant results](https://wordpress.org/plugins/search-by-algolia-instant-relevant-results/) plugin for WordPress, which is licensed under the GPLv2.* == Installation == From your WordPress dashboard: 1. **Visit** Plugins > Add New 2. **Search** for "WP Search with Algolia" 3. **Activate** WP Search with Algolia from your Plugins page 4. **Click** on the new menu item "Algolia Search" and enter your API keys 5. **Read** the step by step [configuration guide](https://github.com/WebDevStudios/wp-search-with-algolia/wiki/Getting-Started) == Frequently Asked Questions == = I see you now have a Pro addon, what features are available with it? = When you purchase a copy of [WP Search with Algolia Pro](https://pluginize.com/plugins/wp-search-with-algolia-pro/) you are getting access to the start of WooCommerce integration as well as Search Engine Optimization mirroring. With WooCommerce, you'll be able to manage settings to start including product information as part of indexed products, including out of box display with both Autocomplete and Instantsearch hit templates. You can also include details like product SKU values, total sales, and ratings to help with index ranking and relevance. With SEO settings, you can configure your content to manage itself in your Algolia indexes based on your "noindex" settings from your dedicated SEO plugins. We intend to continue adding and evolving all the extra features in WP Search with Algolia Pro = Is this plugin a fork? = Yes. The Algolia Team **[no longer supports their original plugin](https://community.algolia.com/wordpress/)**. The engineering team at WebDevStudios has forked the original plugin, and is now maintaining it. = Should I switch to this plugin? = Yes. Because Algolia no longer supports their plugin, you will no longer receive updates. WebDevStudios uses Algolia on many of its projects, and is committed to maintaining this plugin. = What are the minimum requirements? = * Requires WordPress 5.3+ * PHP version 7.4 or greater * MySQL version 5.0 or greater (MySQL 5.6 or greater is recommended) * cURL PHP extension * mbstring PHP extension * OpenSSL greater than 1.0.1 * Some payment gateways require fsockopen support (for IPN access) Visit the [WP Search with Algolia server requirements documentation](https://github.com/WebDevStudios/wp-search-with-algolia/wiki/WP-Search-with-Algolia-plugin-Installation) for a detailed list of server requirements. = Where can I find WP Search with Algolia documentation and user guides? = - For help setting up and configuring WP Search with Algolia please refer to the [user guide](https://github.com/WebDevStudios/wp-search-with-algolia/wiki/WP-Search-with-Algolia-plugin-Installation). - For extending or theming the Autocomplete dropdown, see the [Autocomplete Customization guide](https://github.com/WebDevStudios/wp-search-with-algolia/wiki/Customize-the-Autocomplete-dropdown). - For extending or theming the Instant Search results page, see the [Search Page Customization guide](https://github.com/WebDevStudios/wp-search-with-algolia/wiki/Customize-your-search-page). = Will it work with my theme? = Yes. This plugin should work with most themes that do not override the default WordPress search behavior. Instant Search results page may require some styling to make it match nicely. See the [Search Page Customization](https://github.com/WebDevStudios/wp-search-with-algolia/wiki/Customize-your-search-page). = Where can I report bugs, request features, or contribute to the project? = All development is handled on [GitHub](https://github.com/WebDevStudios/wp-search-with-algolia/issues). == Screenshots == 1. Algolia Settings 2. Search Page Settings 3. Autocomplete Settings 4. InstantSearch Dropdown 5. Search Results == Changelog == Follow along with the changelog on [Github](https://github.com/WebDevStudios/wp-search-with-algolia/releases). = 2.11.3 = * Fixed: minimum requirement discrepancies in readme and defined constant. * Updated: cleaned out some unused CSS selectors from really old functionality. * Updated: make use of `.card` class from WP core for more consistant styling. * Updated: Admin notice around term updating when term is assigned to many posts. = 2.11.2 = * Fixed: Valid Search key checks for new applications. = 2.11.1 = * Fixed: Fatal error if not able to retreive searchable posts index object. * Fixed: Fatal error potential for non-set debounce array index. = 2.11.0 = * Updated: Algolia PHP client (addresses PHP 8.4 compatibility notices) * Updated: UI wording to match Algolia references and Instantsearch notes. * Fixed: Return JSON error instead of echo exception message and continue to throw exception. * Added: Inline documentation for various custom filters and actions. * Added: Output custom debounce values in Autocomplete settings UI. = 2.10.4 = * Fixed: revised asset loading for InstantSearch with Full Site Editing enabled. * Added: Striping styles for Autocomplete table list. = 2.10.3 = * Added: Filter to still output frontend config data when using Instantsearch and FSE Theme. * Added: Filtering of new filter to attempt auto-detection of a FSE theme being in use. = 2.10.2 = * Added: Checkbox option to enable insights in Instantsearch templates. Please review changes to those. = 2.10.1 = * Fixed: Issues around admin notifications on the Autocomplete settings page, introduced in 2.10.0 = 2.10.0 = * Added: Debounce option for Autocomplete. * Added: Initial integration with Health Panel. * Updated: Instantsearch to version 4.78.3 * Updated: Prevent loading of API credentials on frontend when not using Autocomplete or Instantsearch. * Updated: Removed WooCommerce internal post types and taxonomies from Autocomplete list. * Updated: Prevent errors with Yoast SEO function checks. * Updated: Sync'd up internal code for `get_re_index_items_count()` = 2.9.0 = * Added: Instantsearch Template options. Choose between "Legacy" hogan.js/WP Utils templates and "Modern" Javascript string literals. "Modern" is more in line with Algolia Documentation. * Added: ability to customize default Headers for Algolia Search Client configuration. * Added: Initial support for programmatic Secured API key creation. * Updated: Sync'd up `get_items()` methods to allow for specifying specific IDs for posts, terms, and users. * Updated: Instantsearch templates use "Posts per page" amount by default, from WordPress Reading settings. * Updated: Amended Autocomplete settings page to remove more builtin post types that don't need to be available. = 2.8.3 = * Fixed: "Function _load_textdomain_just_in_time was called incorrectly" notices. = 2.8.2 = * Updated: Wording and UI details around the settings pages for better and more accurate reflection. * Updated: Confirmed compatibility with WP 6.7.x * Added: New page regarding Premium support from WebDevStudios. Let's work together. = 2.8.1 = * Updated: WP Search with Algolia Pro features list for version 1.4.0 = 2.8.0 = * Added: Filter to customize Algolia SearchClient configuration with connect/read/write timeouts. * Updated: Prevent table content from being concatenated. Thanks @rodrigo-arias * Updated: Pass `$post_id` to `algolia_get_post_images` filter. * Updated: Confirmed compatibility with WP 6.5 = 2.7.1 = * Fixed: Instantsearch.php template file. "Powered By Algolia" Instantsearch widget wrapped in a check for if the "Remove powered by" option is checked. This should match already working behavior with Autocomplete dropdown. = 2.7.0 = * Updated: Moved post sync action from `save_post` to `wp_after_insert_post`. This allows for the sync to wait until after post meta and terms have been updated. Removes need to click save twice to sync everything. * Updated: WP Search with Algolia feature list. = 2.6.2 = * Fixed: More performance updates and resolutions around WP All Import. = 2.6.1 = * Fixed: Performance issues related to delete operations. * Fixed: Performance issues around WP All Import. = 2.6.0 = * Added: Support for syncing imported items when "fast mode" from WP All Import enabled. * Added: Support for updating child posts if parent post's slug has been updated. * Added: Support for updating posts when an associated term has been updatd. * Added: Wait for delete operations to complete before moving to updates. * Updated: Algolia Search library to 4.18.x * Updated: InstantSearch library to 4.56.x = 2.5.4 = * Updated: Ensure reindexing completes when using the from_batch flag with CLI. * Updated: Assigned Algolia_Admin instance to a property for access elsewhere. = 2.5.3 = * Updated: Autocomplete template file with user link fix when cmd/ctrl clicking. * Updated: Class method visibility from protected to public. = 2.5.2 = * Updated: Fixed hits per page configuration for instantsearch * Added: Custom hook for settings page override. = 2.5.1 = * Updated readme.txt with more plugin information. * Repositioned help info on settings screens. = 2.5.0 = * Introduction of WP Search with Algolia Pro availability. * Added `algolia_custom_template_location` filter to allow specifying custom template locations besides just your active theme. * Templates: added action hooks at the end of Autocomplete and Instantsearch hit template blocks. * Updated `algolia_changes_watchers` filter to also receive the current indices. * Added watcher support for term and user meta updates. * Updated bundled CSS to better match selectors for default used widgets in the templates. * Clarified some details around Autocomplete settings and what can be done in each setting state. * Updated admin menu icon to use Algolia logo when no settings configured. ================================================ FILE: algolia.php ================================================ * @since 1.1.0 * * @return bool */ function algolia_php_version_check() { if ( version_compare( PHP_VERSION, ALGOLIA_MIN_PHP_VERSION, '<' ) ) { return false; } return true; } /** * Check for required WordPress version. * * @author WebDevStudios * @since 1.1.0 * * @return bool */ function algolia_wp_version_check() { if ( version_compare( $GLOBALS['wp_version'], ALGOLIA_MIN_WP_VERSION, '<' ) ) { return false; } return true; } /** * Check if WP Search with Algolia Pro is active. * * @author Webdevstudios * @since 2.5.0 * * @return bool */ function algolia_is_pro_active() { if ( ! defined( 'WPSWA_PRO_VERSION' ) ) { return false; } return true; } /** * Admin notices if requirements aren't met. * * @author WebDevStudios * @since 1.1.0 */ function algolia_requirements_error_notice() { $notices = []; if ( ! algolia_php_version_check() ) { $notices[] = sprintf( // translators: placeholder 1 is minimum required PHP version, placeholder 2 is installed PHP version. esc_html__( 'Algolia plugin requires PHP %1$s or higher. You’re still on %2$s.', 'wp-search-with-algolia' ), esc_html( ALGOLIA_MIN_PHP_VERSION ), esc_html( PHP_VERSION ) ); } if ( ! algolia_wp_version_check() ) { $notices[] = sprintf( // translators: placeholder 1 is minimum required WordPress version, placeholder 2 is installed WordPress version. esc_html__( 'Algolia plugin requires at least WordPress in version %1$s, You are on %2$s.', 'wp-search-with-algolia' ), esc_html( ALGOLIA_MIN_WP_VERSION ), esc_html( $GLOBALS['wp_version'] ) ); } foreach ( $notices as $notice ) { echo '

' . esc_html( $notice ) . '

'; } } /** * I18n. * * @author WebDevStudios * @since 1.0.0 */ function algolia_load_textdomain() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- This is a legitimate use of a global filter. $locale = apply_filters( 'plugin_locale', get_locale(), 'wp-search-with-algolia' ); load_textdomain( 'wp-search-with-algolia', WP_LANG_DIR . '/wp-search-with-algolia/wp-search-with-algolia-' . $locale . '.mo' ); load_plugin_textdomain( 'wp-search-with-algolia', false, plugin_basename( dirname( __FILE__ ) ) . '/languages/' ); } add_action( 'init', 'algolia_load_textdomain' ); if ( algolia_php_version_check() && algolia_wp_version_check() ) { require_once ALGOLIA_PATH . 'classmap.php'; $algolia = Algolia_Plugin_Factory::create(); if ( defined( 'WP_CLI' ) && WP_CLI ) { WP_CLI::add_command( 'algolia', new Algolia_CLI() ); } } else { add_action( 'admin_notices', 'algolia_requirements_error_notice' ); } ================================================ FILE: classmap.php ================================================ * @since 1.0.0 * * @package WebDevStudios\WPSWA */ if ( ! defined( 'ALGOLIA_PATH' ) ) { exit(); } /** * Filters whether to include the Algolia PHP API Client library. * * @since 1.0.0 * @deprecated 2.3.2 No longer necessary as the Algolia PHP API Client library is now prefixed. * * @param bool $true Include the Algolia PHP API Client library. */ apply_filters_deprecated( 'algolia_should_require_search_client', [ true ], '2.3.2', '', 'The "algolia_should_require_search_client" filter is deprecated and no longer has any effect.', ); // Autoload vendor dependencies, that have been prefixed to prevent namespace collision. require_once ALGOLIA_PATH . 'vendor_prefixed/autoload.php'; require_once ALGOLIA_PATH . 'includes/factories/class-algolia-http-client-interface-factory.php'; require_once ALGOLIA_PATH . 'includes/factories/class-algolia-search-client-factory.php'; require_once ALGOLIA_PATH . 'includes/factories/class-algolia-plugin-factory.php'; require_once ALGOLIA_PATH . 'includes/class-algolia-api.php'; require_once ALGOLIA_PATH . 'includes/class-algolia-autocomplete-config.php'; require_once ALGOLIA_PATH . 'includes/class-algolia-cli.php'; require_once ALGOLIA_PATH . 'includes/class-algolia-compatibility.php'; require_once ALGOLIA_PATH . 'includes/class-algolia-plugin.php'; require_once ALGOLIA_PATH . 'includes/class-algolia-search.php'; require_once ALGOLIA_PATH . 'includes/class-algolia-settings.php'; require_once ALGOLIA_PATH . 'includes/class-algolia-template-loader.php'; require_once ALGOLIA_PATH . 'includes/class-algolia-utils.php'; require_once ALGOLIA_PATH . 'includes/class-algolia-styles.php'; require_once ALGOLIA_PATH . 'includes/class-algolia-scripts.php'; require_once ALGOLIA_PATH . 'includes/indices/class-algolia-index.php'; require_once ALGOLIA_PATH . 'includes/indices/class-algolia-index-replica.php'; require_once ALGOLIA_PATH . 'includes/indices/class-algolia-searchable-posts-index.php'; require_once ALGOLIA_PATH . 'includes/indices/class-algolia-posts-index.php'; require_once ALGOLIA_PATH . 'includes/indices/class-algolia-terms-index.php'; require_once ALGOLIA_PATH . 'includes/indices/class-algolia-users-index.php'; require_once ALGOLIA_PATH . 'includes/watchers/class-algolia-changes-watcher.php'; require_once ALGOLIA_PATH . 'includes/watchers/class-algolia-post-changes-watcher.php'; require_once ALGOLIA_PATH . 'includes/watchers/class-algolia-term-changes-watcher.php'; require_once ALGOLIA_PATH . 'includes/watchers/class-algolia-user-changes-watcher.php'; require_once ALGOLIA_PATH . 'includes/utilities/class-algolia-health-panel.php'; require_once ALGOLIA_PATH . 'includes/utilities/class-algolia-template-utils.php'; require_once ALGOLIA_PATH . 'includes/utilities/class-algolia-version-utils.php'; require_once ALGOLIA_PATH . 'includes/utilities/class-algolia-update-messages.php'; if ( is_admin() ) { require_once ALGOLIA_PATH . 'includes/admin/class-algolia-admin.php'; require_once ALGOLIA_PATH . 'includes/admin/class-algolia-admin-page-settings.php'; require_once ALGOLIA_PATH . 'includes/admin/class-algolia-admin-page-autocomplete.php'; require_once ALGOLIA_PATH . 'includes/admin/class-algolia-admin-page-native-search.php'; require_once ALGOLIA_PATH . 'includes/admin/class-algolia-admin-page-woocommerce.php'; require_once ALGOLIA_PATH . 'includes/admin/class-algolia-admin-page-premium-support.php'; require_once ALGOLIA_PATH . 'includes/admin/class-algolia-admin-page-seo.php'; require_once ALGOLIA_PATH . 'includes/admin/class-algolia-admin-template-notices.php'; } ================================================ FILE: composer.json ================================================ { "name": "webdevstudios/wp-search-with-algolia", "version": "2.11.3", "description": "Integrate the powerful Algolia search service with WordPress.", "authors": [ { "name": "WebDevStudios", "email": "contact@webdevstudios.com" } ], "license": "GPL-3.0", "keywords": [ "algolia" ], "homepage": "https://github.com/WebDevStudios/wp-search-with-algolia", "type": "wordpress-plugin", "minimum-stability": "dev", "prefer-stable": true, "require": { "php": ">=7.4", "composer/installers": "~1.0" }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "^0.7.1", "phpcompatibility/phpcompatibility-wp": "^2.1.2", "wp-coding-standards/wpcs": "^2.3.0", "psr/http-message": "^1.0", "psr/log": "^1.1", "psr/simple-cache": "^1.0", "brianhenryie/strauss": "^0.11.1", "php-stubs/wp-cli-stubs": "^2.7", "php-stubs/wordpress-stubs": "^6.1", "algolia/algoliasearch-client-php": "3.4.2" }, "extra": { "installer-name": "wp-search-with-algolia", "strauss": { "target_directory": "vendor_prefixed", "namespace_prefix": "WebDevStudios\\WPSWA\\", "classmap_prefix": "WDS_WPSWA_", "constant_prefix": "WDS_WPSWA_", "packages": [ "algolia/algoliasearch-client-php", "psr/http-message", "psr/log", "psr/simple-cache" ], "override_autoload": { }, "exclude_from_copy": { "packages": [ ], "namespaces": [ ], "file_patterns": [ ] }, "exclude_from_prefix": { "packages": [ ], "namespaces": [ ], "file_patterns": [ ] }, "namespace_replacement_patterns" : { }, "delete_vendor_files": false }, "copy-file": { }, "copy-file-dev": { } }, "scripts": { "lint": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs --standard=phpcs.xml --extensions=php .", "lint:fix": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcbf --standard=phpcs.xml --extensions=php .", "post-install-cmd": [ "@php ./vendor/bin/strauss" ], "post-update-cmd": [ "@php ./vendor/bin/strauss" ] }, "config": { "allow-plugins": { "composer/installers": true, "dealerdirect/phpcodesniffer-composer-installer": true } } } ================================================ FILE: css/algolia-autocomplete.css ================================================ .algolia-autocomplete { z-index: 999999 !important; } .aa-dropdown-menu { /* we set the width in JS */ font-family: sans-serif; background-color: #fff; border-top: none; border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; box-shadow: 0 1px 2px rgba(0,0,0,.25),0 0 1px rgba(0,0,0,.35); } .aa-dropdown-menu:after { content: " "; display: block; clear: both; } .aa-dropdown-menu .aa-input, .aa-dropdown-menu .aa-hint { width: 100%; } .aa-dropdown-menu .aa-hint { color: #999; } /* Font */ .aa-dropdown-menu { color: #1a1a1a; font-size: 12px; } .aa-dropdown-menu a { font-size: 12px; color: #1a1a1a; font-weight: normal; text-decoration: none; } .aa-dropdown-menu a:hover { text-decoration: none; } /* Header */ .aa-dropdown-menu .autocomplete-header { margin: 0 14px; line-height: 3em; border-bottom: 1px solid rgba(0,0,0,.05); } .aa-dropdown-menu .autocomplete-header-title, .aa-dropdown-menu .autocomplete-header-more { letter-spacing: 1px; text-transform: uppercase; font-weight: bold; } .aa-dropdown-menu .autocomplete-header-title { float: left; } .aa-dropdown-menu .autocomplete-header-more { float: right; } .aa-dropdown-menu .autocomplete-header-more a { color: rgba(0,0,0,.3); font-weight: bold; } .aa-dropdown-menu .autocomplete-header-more a:hover { color: rgba(0,0,0,.4); } /* Suggestion */ .aa-dropdown-menu .aa-suggestion { padding: 5px 0; } .aa-dropdown-menu .aa-suggestion:after { visibility: hidden; display: block; font-size: 0; content: " "; clear: both; height: 0; } .aa-dropdown-menu .aa-suggestion em { color: #174d8c; background: rgba(143,187,237,.1); font-style: normal; } .aa-dropdown-menu .aa-suggestion .suggestion-post-title { font-weight: bold; display: block; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; } .aa-dropdown-menu .aa-suggestion .suggestion-post-content { color: #63676d; display: block; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; } .aa-dropdown-menu .aa-suggestion .suggestion-post-content em { padding: 0 0 1px; background: inherit; box-shadow: inset 0 -2px 0 0 rgba(69,142,225,.8); color: inherit; } .aa-dropdown-menu .aa-suggestion.aa-cursor { background-color: #f9f9f9; } .aa-dropdown-menu a.suggestion-link { display: block; padding: 0 14px; } .aa-dropdown-menu a.suggestion-link.user-suggestion-link { line-height: 32px; } .aa-dropdown-menu a.suggestion-link svg { vertical-align: middle; fill: rgba(0,0,0,.3); float: left; } .aa-dropdown-menu .suggestion-post-thumbnail { float: left; margin-right: 5px; margin-bottom: 5px; border-radius: 3px; width: 32px; height: 32px; } .aa-dropdown-menu .suggestion-user-thumbnail { float: left; margin-right: 5px; margin-bottom: 5px; border-radius: 16px; width: 32px; height: 32px; } /* Footer */ .aa-dropdown-menu .autocomplete-footer-branding { padding: 15px 14px 0px; float: right; color: rgba(0,0,0,.3); margin-bottom: 7px; } /* Clearfix */ .aa-dropdown-menu .clear { clear: both; } /* Empty */ .autocomplete-empty { clear: both; padding: 15px; } .autocomplete-empty .empty-query { font-weight: bold; } ================================================ FILE: css/algolia-instantsearch.css ================================================ #ais-wrapper { display: flex; } #ais-main { padding: 1rem; width: 100%; } #ais-facets { width: 40%; padding: 1rem; } .ais-facets { margin-bottom: 2rem; padding: 0; } .ais-facets ul { list-style: none; padding-left: 0; margin-left: 0; } .ais-facets li { margin-left: 0; } .ais-clearfix { clear: both; } .algolia-search-box-wrapper { position: relative; } .algolia-search-box-wrapper .search-icon { position: absolute; left: 0px; top: 14px; fill: #21a4d7; } #algolia-search-box { margin-bottom: 3rem; } #algolia-search-box input { border: none; border-bottom: 2px solid #21a4d7; background: transparent; width: 100%; line-height: 30px; font-size: 22px; padding: 10px 0 10px 30px; font-weight: 200; box-sizing: border-box; outline: none; box-shadow: none; appearance: none; -webkit-appearance: none; -moz-appearance: none; -ms-appearance: none; } .ais-SearchBox-form { display: block; position: relative; } .ais-SearchBox-submit[hidden], .ais-SearchBox-reset[hidden], .ais-SearchBox-loadingIndicator[hidden] { display: none; } #algolia-powered-by { position: absolute; top: 60px; right: 0; font-size: 14px; text-align: right; } .ais-Stats { position: absolute; top: 60px; font-size: 14px; } .ais-Hits-list { list-style: none; padding-left: 0; margin-left: 0; } .ais-Hits-item { /* hit item */ margin: 0 0 2rem 0; } .ais-Hits-item h2 { margin: 0; } .ais-Hits-item em, .ais-Hits-item a em, .ais-Hits-item mark, .ais-Hits-item a mark { font-style: normal; background: #fffbcc; border-radius: 2px; } .ais-hits--thumbnail { float: left; margin-right: 2rem; } .ais-hits--content { overflow: hidden; } .ais-hits--thumbnail img { border-radius: 3px; } .ais-Pagination { margin: 0; } .ais-Pagination-list { margin-left: 0; } .ais-Pagination-item { /* Pagination item */ display: inline-block; padding: 3px; } .ais-Pagination-item--disabled { /* disabled Pagination item */ display: none; } .ais-Pagination-item--selected { font-weight: bold; } .ais-Menu-item--selected { /* active list item */ font-weight: bold; } .ais-HierarchicalMenu-item--selected { /* Hierarchical Menu: Categories */ font-weight: bold; } .ais-Menu-count, .ais-HierarchicalMenu-count, .ais-RefinementList-count { margin-left: 5px; } .ais-HierarchicalMenu-list--child { /* item list level 1 */ margin-left: 10px; } .ais-RangeSlider .rheostat { overflow: visible; margin-top: 40px; margin-bottom: 40px; } .ais-RangeSlider .rheostat-background { height: 6px; top: 0px; width: 100%; } .ais-RangeSlider .rheostat-handle { margin-left: -12px; top: -7px; } .ais-RangeSlider .rheostat-background { position: relative; background-color: #fff; border: 1px solid #003dff; } .ais-RangeSlider .rheostat-progress { position: absolute; top: 1px; height: 4px; background-color: #333; } .rheostat-handle { position: relative; z-index: 1; width: 20px; height: 20px; background-color: #fff; border: 1px solid #333; border-radius: 50%; cursor: -webkit-grab; cursor: grab; } .rheostat-marker { margin-left: -1px; position: absolute; width: 1px; height: 5px; background-color: #aaa; } .rheostat-marker--large { height: 9px; } .rheostat-value { margin-left: 50%; padding-top: 15px; position: absolute; text-align: center; -webkit-transform: translateX(-50%); transform: translateX(-50%); } .rheostat-tooltip { margin-left: 50%; position: absolute; top: -22px; text-align: center; -webkit-transform: translateX(-50%); transform: translateX(-50%); } .ais-RatingMenu-item { /* list item */ vertical-align: middle; } .ais-RatingMenu-item--selected { /* active list item */ font-weight: bold; } .ais-RatingMenu-starIcon { /* item star */ display: inline-block; width: 1em; height: 1em; } .ais-RatingMenu-starIcon:before { content: '\2605'; color: #fbae00; } .ais-RatingMenu-starIcon--empty { /* empty star */ display: inline-block; width: 1em; height: 1em; } .ais-RatingMenu-starIcon--empty:before { content: '\2606'; color: #fbae00; } .ais-RatingMenu-item--disabled .ais-star-rating--star:before { color: #c9c9c9; } .ais-RatingMenu-item--disabled .ais-star-rating--star__empty:before { color: #c9c9c9; } .ais-root__collapsible .ais-header { cursor: pointer; } .ais-root__collapsed .ais-body, .ais-root__collapsed .ais-footer { display: none; } /* Responsive */ @media only screen and (max-width: 1000px) { #ais-facets { display: none; } .ais-hits--thumbnail img { width: 100% !important; } .ais-Hits-item { border-bottom: 1px solid gainsboro; padding-bottom: 23px; } } @media only screen and (max-width: 500px) { .ais-hits--thumbnail { margin-right: 0 !important; margin-bottom: 10px; float: none !important; } } ================================================ FILE: css/index.php ================================================ * @since 1.0.0 * * @package WebDevStudios\WPSWA */ /** * Class Algolia_Admin_Page_Autocomplete * * @since 1.0.0 */ class Algolia_Admin_Page_Autocomplete { /** * Admin page slug. * * @author WebDevStudios * @since 1.0.0 * * @var string */ private $slug = 'algolia'; /** * Admin page capabilities. * * @author WebDevStudios * @since 1.0.0 * * @var string */ private $capability = 'manage_options'; /** * Admin page section. * * @author WebDevStudios * @since 1.0.0 * * @var string */ private $section = 'algolia_section_autocomplete'; /** * Admin page option group. * * @author WebDevStudios * @since 1.0.0 * * @var string */ private $option_group = 'algolia_autocomplete'; /** * The Algolia_Settings object. * * @author WebDevStudios * @since 1.0.0 * * @var Algolia_Settings */ private $settings; /** * The Algolia_Autocomplete_Config object. * * @since 1.0.0 * * @var Algolia_Autocomplete_Config */ private $autocomplete_config; /** * Algolia_Admin_Page_Autocomplete constructor. * * @author WebDevStudios * @since 1.0.0 * * @param Algolia_Settings $settings The Algolia_Settings object. * @param Algolia_Autocomplete_Config $autocomplete_config The Algolia_Autocomplete_Config object. */ public function __construct( Algolia_Settings $settings, Algolia_Autocomplete_Config $autocomplete_config ) { $this->settings = $settings; $this->autocomplete_config = $autocomplete_config; add_action( 'admin_menu', array( $this, 'add_page' ) ); add_action( 'admin_init', array( $this, 'add_settings' ) ); add_action( 'admin_notices', array( $this, 'display_errors' ) ); // @todo: Listen for de-index to remove from autocomplete. } /** * Add menu pages. * * @author WebDevStudios * @since 1.0.0 */ public function add_page() { add_menu_page( esc_html__( 'Algolia Search', 'wp-search-with-algolia' ), esc_html__( 'Algolia Search', 'wp-search-with-algolia' ), 'manage_options', 'algolia', array( $this, 'display_page' ), 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIGlkPSJMYXllcl8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MDAgNTAwLjM0Ij48ZGVmcz48c3R5bGU+LmNscy0xe2ZpbGw6IzAwM2RmZjt9PC9zdHlsZT48L2RlZnM+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMjUwLDBDMTEzLjM4LDAsMiwxMTAuMTYsLjAzLDI0Ni4zMmMtMiwxMzguMjksMTEwLjE5LDI1Mi44NywyNDguNDksMjUzLjY3LDQyLjcxLC4yNSw4My44NS0xMC4yLDEyMC4zOC0zMC4wNSwzLjU2LTEuOTMsNC4xMS02LjgzLDEuMDgtOS41MmwtMjMuMzktMjAuNzRjLTQuNzUtNC4yMi0xMS41Mi01LjQxLTE3LjM3LTIuOTItMjUuNSwxMC44NS01My4yMSwxNi4zOS04MS43NiwxNi4wNC0xMTEuNzUtMS4zNy0yMDIuMDQtOTQuMzUtMjAwLjI2LTIwNi4xLDEuNzYtMTEwLjMzLDkyLjA2LTE5OS41NSwyMDIuOC0xOTkuNTVoMjAyLjgzVjQwNy42OGwtMTE1LjA4LTEwMi4yNWMtMy43Mi0zLjMxLTkuNDMtMi42Ni0xMi40MywxLjMxLTE4LjQ3LDI0LjQ2LTQ4LjU2LDM5LjY3LTgxLjk4LDM3LjM2LTQ2LjM2LTMuMi04My45Mi00MC41Mi04Ny40LTg2Ljg2LTQuMTUtNTUuMjgsMzkuNjUtMTAxLjU4LDk0LjA3LTEwMS41OCw0OS4yMSwwLDg5Ljc0LDM3Ljg4LDkzLjk3LDg2LjAxLC4zOCw0LjI4LDIuMzEsOC4yOCw1LjUzLDExLjEzbDI5Ljk3LDI2LjU3YzMuNCwzLjAxLDguOCwxLjE3LDkuNjMtMy4zLDIuMTYtMTEuNTUsMi45Mi0yMy42LDIuMDctMzUuOTUtNC44My03MC4zOS02MS44NC0xMjcuMDEtMTMyLjI2LTEzMS4zNS04MC43My00Ljk4LTE0OC4yMyw1OC4xOC0xNTAuMzcsMTM3LjM1LTIuMDksNzcuMTUsNjEuMTIsMTQzLjY2LDEzOC4yOCwxNDUuMzYsMzIuMjEsLjcxLDYyLjA3LTkuNDIsODYuMi0yNi45N2wxNTAuMzYsMTMzLjI5YzYuNDUsNS43MSwxNi42MiwxLjE0LDE2LjYyLTcuNDhWOS40OUM1MDAsNC4yNSw0OTUuNzUsMCw0OTAuNTEsMEgyNTBaIi8+PC9zdmc+' ); add_submenu_page( 'algolia', esc_html__( 'Autocomplete', 'wp-search-with-algolia' ), esc_html__( 'Autocomplete', 'wp-search-with-algolia' ), $this->capability, $this->slug, array( $this, 'display_page' ) ); } /** * Add and register settings. * * @author WebDevStudios * @since 1.0.0 */ public function add_settings() { add_settings_section( $this->section, null, array( $this, 'print_section_settings' ), $this->slug ); add_settings_field( 'algolia_autocomplete_enabled', esc_html__( 'Enable Autocomplete', 'wp-search-with-algolia' ), array( $this, 'autocomplete_enabled_callback' ), $this->slug, $this->section ); add_settings_field( 'algolia_autocomplete_debounce', esc_html__( 'Autocomplete Debounce', 'wp-search-with-algolia' ), array( $this, 'autocomplete_debounce_callback' ), $this->slug, $this->section ); add_settings_field( 'algolia_autocomplete_config', esc_html__( 'Autocomplete Config', 'wp-search-with-algolia' ), array( $this, 'autocomplete_config_callback' ), $this->slug, $this->section ); register_setting( $this->option_group, 'algolia_autocomplete_enabled', array( $this, 'sanitize_autocomplete_enabled' ) ); register_setting( $this->option_group, 'algolia_autocomplete_debounce', array( $this, 'sanitize_autocomplete_debounce' ) ); register_setting( $this->option_group, 'algolia_autocomplete_config', array( $this, 'sanitize_autocomplete_config' ) ); } /** * Callback to print the autocomplete enabled checkbox. * * @author WebDevStudios * @since 1.0.0 */ public function autocomplete_enabled_callback() { $value = $this->settings->get_autocomplete_enabled(); $indices = $this->autocomplete_config->get_form_data(); $checked = 'yes' === $value ? 'checked ' : ''; $disabled = empty( $indices ) ? 'disabled ' : ''; ?> /> * @since 2.10.0 */ public function autocomplete_debounce_callback() { $value = $this->settings->get_autocomplete_debounce(); $indices = $this->autocomplete_config->get_form_data(); ?> />

* @since 1.0.0 * * @param string $value The original value. * * @return string */ public function sanitize_autocomplete_enabled( $value ) { add_settings_error( $this->option_group, 'autocomplete_enabled', esc_html__( 'Autocomplete configuration has been saved. Make sure to hit the "re-index" buttons of the different indices that are not indexed yet.', 'wp-search-with-algolia' ), 'updated' ); return 'yes' === $value ? 'yes' : 'no'; } /** * Sanitize the Autocomplete debounce setting. * * @author WebDevStudios * @since 2.10.0 * * @param int $value The original value. * * @return int The sanitized value. */ public function sanitize_autocomplete_debounce( $value ) { return intval( $value ); } /** * Autocomplete Config Callback. * * @author WebDevStudios * @since 1.0.0 */ public function autocomplete_config_callback() { $indices = $this->autocomplete_config->get_form_data(); require_once dirname( __FILE__ ) . '/partials/page-autocomplete-config.php'; } /** * Sanitize Autocomplete Config. * * @author WebDevStudios * @since 1.0.0 * * @param array $values Array of autocomplete config values. * * @return array|mixed */ public function sanitize_autocomplete_config( $values ) { return $this->autocomplete_config->sanitize_form_data( $values ); } /** * Display the page. * * @author WebDevStudios * @since 1.0.0 */ public function display_page() { require_once dirname( __FILE__ ) . '/partials/page-autocomplete.php'; } /** * Display the errors. * * @author WebDevStudios * @since 1.0.0 * * @return void */ public function display_errors() { settings_errors( $this->option_group ); if ( defined( 'ALGOLIA_HIDE_HELP_NOTICES' ) && ALGOLIA_HIDE_HELP_NOTICES ) { return; } $is_enabled = 'yes' === $this->settings->get_autocomplete_enabled(); $indices = $this->autocomplete_config->get_config(); if ( true === $is_enabled && empty( $indices ) ) { // translators: placeholder contains the URL to the autocomplete configuration page. $message = sprintf( __( 'Please select one or multiple indices on the Algolia: Autocomplete configuration page.', 'wp-search-with-algolia' ), esc_url( admin_url( 'admin.php?page=' . $this->slug ) ) ); echo '

' . esc_html__( 'You have enabled the Algolia Autocomplete feature but did not choose any index to search in.', 'wp-search-with-algolia' ) . '

' . wp_kses_post( $message ) . '

'; } } /** * Prints the section text. * * @author WebDevStudios * @since 1.0.0 */ public function print_section_settings() { echo '

' . esc_html__( 'Autocomplete adds a search-as-you-type dropdown to your search field(s).', 'wp-search-with-algolia' ) . '

'; echo '

' . esc_html__( 'Enabling Autocomplete adds the functionality to your site\'s frontend search. Indexing and settings pushes can be done regardless of enabled status.', 'wp-search-with-algolia' ) . '

'; } } ================================================ FILE: includes/admin/class-algolia-admin-page-native-search.php ================================================ * @since 1.0.0 * * @package WebDevStudios\WPSWA */ /** * Class Algolia_Admin_Page_Native_Search * * @since 1.0.0 */ class Algolia_Admin_Page_Native_Search { /** * Admin page slug. * * @author WebDevStudios * @since 1.0.0 * * @var string */ private $slug = 'algolia-search-page'; /** * Admin page capabilities. * * @author WebDevStudios * @since 1.0.0 * * @var string */ private $capability = 'manage_options'; /** * Admin page section. * * @author WebDevStudios * @since 1.0.0 * * @var string */ private $section = 'algolia_section_native_search'; /** * Admin page option group. * * @author WebDevStudios * @since 1.0.0 * * @var string */ private $option_group = 'algolia_native_search'; /** * The Algolia_Plugin instance. * * @author WebDevStudios * @since 1.0.0 * * @var Algolia_Plugin */ private $plugin; /** * Algolia_Admin_Page_Native_Search constructor. * * @author WebDevStudios * @since 1.0.0 * * @param Algolia_Plugin $plugin The Algolia_Plugin instance. */ public function __construct( Algolia_Plugin $plugin ) { $this->plugin = $plugin; add_action( 'admin_menu', array( $this, 'add_page' ) ); add_action( 'admin_init', array( $this, 'add_settings' ) ); add_action( 'admin_notices', array( $this, 'display_errors' ) ); } /** * Add submenu page. * * @author WebDevStudios * @since 1.0.0 */ public function add_page() { add_submenu_page( 'algolia', esc_html__( 'Search Page', 'wp-search-with-algolia' ), esc_html__( 'Search Page', 'wp-search-with-algolia' ), $this->capability, $this->slug, array( $this, 'display_page' ), 0 ); } /** * Add settings. * * @author WebDevStudios * @since 1.0.0 */ public function add_settings() { add_settings_section( $this->section, null, array( $this, 'print_section_settings' ), $this->slug ); add_settings_field( 'algolia_override_native_search', esc_html__( 'Search results', 'wp-search-with-algolia' ), array( $this, 'override_native_search_callback' ), $this->slug, $this->section ); add_settings_field( 'algolia_instantsearch_template_version', esc_html__( 'Instantsearch Template version', 'wp-search-with-algolia' ), [ $this, 'instantsearch_template_version' ], $this->slug, $this->section ); register_setting( $this->option_group, 'algolia_override_native_search', array( $this, 'sanitize_override_native_search' ) ); register_setting( $this->option_group, 'algolia_instantsearch_template_version', [ 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'default' => 'legacy', ] ); } /** * Override native search callback. * * @author WebDevStudios * @since 1.0.0 */ public function override_native_search_callback() { $value = $this->plugin->get_settings()->get_override_native_search(); require_once dirname( __FILE__ ) . '/partials/form-override-search-option.php'; } /** * Get Instantsearch template version * * @author WebDevStudios * @since 2.9.0 */ public function instantsearch_template_version() { $value = $this->plugin->get_settings()->get_instantsearch_template_version(); require_once dirname( __FILE__ ) . '/partials/form-override-search-version-option.php'; } /** * Sanitize override native search. * * @author WebDevStudios * @since 1.0.0 * * @param string $value The value to sanitize. * * @return array */ public function sanitize_override_native_search( $value ) { if ( 'backend' === $value ) { add_settings_error( $this->option_group, 'native_search_enabled', esc_html__( 'WordPress search is now based on Algolia!', 'wp-search-with-algolia' ), 'updated' ); } elseif ( 'instantsearch' === $value ) { add_settings_error( $this->option_group, 'native_search_enabled', esc_html__( 'WordPress search is now based on Algolia instantsearch.js!', 'wp-search-with-algolia' ), 'updated' ); } else { $value = 'native'; add_settings_error( $this->option_group, 'native_search_disabled', esc_html__( 'You chose to keep the WordPress native search instead of Algolia. If you are using the autocomplete feature of the plugin we highly recommend you turn Algolia search on instead of the WordPress native search.', 'wp-search-with-algolia' ), 'updated' ); } return $value; } /** * Display the page. * * @author WebDevStudios * @since 1.0.0 */ public function display_page() { require_once dirname( __FILE__ ) . '/partials/page-search.php'; } /** * Display the errors. * * @author WebDevStudios * @since 1.0.0 * * @return void */ public function display_errors() { settings_errors( $this->option_group ); if ( defined( 'ALGOLIA_HIDE_HELP_NOTICES' ) && ALGOLIA_HIDE_HELP_NOTICES ) { return; } $settings = $this->plugin->get_settings(); if ( ! $settings->should_override_search_in_backend() && ! $settings->should_override_search_with_instantsearch() ) { return; } $maybe_get_page = filter_input( INPUT_GET, 'page', FILTER_SANITIZE_SPECIAL_CHARS ); $searchable_posts_index = $this->plugin->get_index( 'searchable_posts' ); if ( empty( $searchable_posts_index ) ) { return; } if ( false === $searchable_posts_index->is_enabled() && ( ! empty( $maybe_get_page ) ) && $maybe_get_page === $this->slug ) { // translators: placeholder contains the link to the indexing page. $message = sprintf( __( 'Searchable posts index needs to be checked on the Algolia: Indexing page for the search results to be powered by Algolia.', 'wp-search-with-algolia' ), esc_url( admin_url( 'admin.php?page=algolia-indexing' ) ) ); echo '

' . wp_kses_post( $message ) . '

'; } } /** * Prints the section text. * * @author WebDevStudios * @since 1.0.0 */ public function print_section_settings() { echo '

' . esc_html__( 'By enabling these settings to override the native WordPress search, your search results will be powered by Algolia\'s typo-tolerant & relevant search algorithms.', 'wp-search-with-algolia' ) . '

'; echo '

' . sprintf( '%1$s - %2$s', esc_html__( 'Re-index All Content', 'wp-search-with-algolia' ), esc_html__( 'Resubmit all of your content to the Algolia search API. Search results will be updated once the re-index has completed.', 'wp-search-with-algolia' ) ) . '

'; echo '

' . sprintf( '%1$s - %2$s %3$s', esc_html__( 'Push Settings', 'wp-search-with-algolia' ), esc_html__( 'Sync your search index settings to code-based overrides and plugin defaults.', 'wp-search-with-algolia' ), esc_html__( 'WARNING this will override or reset configuration changes originally made within your Algolia dashboard.', 'wp-search-with-algolia' ) ) . '

'; // @Todo: replace this with a check on the searchable_posts_index. $indices = $this->plugin->get_indices( array( 'enabled' => true, 'contains' => 'posts', ) ); if ( empty( $indices ) ) { echo '

' . esc_html( __( 'You have no index containing only posts yet. Please index some content with the "Re-index All Content" button above.', 'wp-search-with-algolia' ) ) . '

'; } } } ================================================ FILE: includes/admin/class-algolia-admin-page-premium-support.php ================================================ * @since 2.8.2 * @package WebDevStudios\WPSWA */ /** * Class Algolia_Admin_Page_Premium_Support * * @since 2.8.2 */ class Algolia_Admin_Page_Premium_Support { /** * Admin page slug. * * @author WebDevStudios * @since 2.8.2 * @var string */ private $slug = 'algolia-account-premium-support'; /** * Admin page capabilities. * * @author WebDevStudios * @since 2.8.2 * @var string */ private $capability = 'manage_options'; /** * Admin page section. * * @author WebDevStudios * @since 2.8.2 * @var string */ private $section = 'algolia_section_premium_support'; /** * Admin page option group. * * @author WebDevStudios * @since 2.8.2 * @var string */ private $option_group = 'algolia_settings'; /** * The Algolia_Plugin instance. * * @author WebDevStudios * @since 2.8.2 * @var Algolia_Plugin */ private $plugin; /** * Algolia_Admin_Page_Premium_Support constructor. * * @param Algolia_Plugin $plugin The Algolia_Plugin instance. * * @since 2.8.2 * @author WebDevStudios */ public function __construct( Algolia_Plugin $plugin ) { $this->plugin = $plugin; add_action( 'admin_menu', [ $this, 'add_page' ] ); add_action( 'admin_init', [ $this, 'add_settings' ] ); } /** * Add admin menu page. * * @since 2.8.2 * * @author WebDevStudios * * @return string|void The resulting page's hook_suffix. */ public function add_page() { $api = $this->plugin->get_api(); $parent_slug = ! $api->is_reachable() ? 'algolia-account-settings' : 'algolia'; add_submenu_page( $parent_slug, esc_html__( 'Premium Support from WebDevStudios', 'wp-search-with-algolia' ), esc_html__( 'Premium Support', 'wp-search-with-algolia' ), $this->capability, $this->slug, [ $this, 'display_page' ] ); } /** * Add settings. * * @author WebDevStudios * @since 2.8.2 */ public function add_settings() { add_settings_section( $this->section, null, [ $this, 'print_section_settings' ], $this->slug ); } /** * Display the page. * * @author WebDevStudios * @since 2.8.2 */ public function display_page() { require_once dirname( __FILE__ ) . '/partials/form-options-premium-support.php'; } /** * Print the settings section. * * @author WebDevStudios * @since 2.8.2 */ public function print_section_settings() { } } ================================================ FILE: includes/admin/class-algolia-admin-page-seo.php ================================================ * @since 2.5.0 * @package WebDevStudios\WPSWA */ /** * Class Algolia_Admin_Page_SEO * * @since 2.5.0 */ class Algolia_Admin_Page_SEO { /** * Admin page slug. * * @author WebDevStudios * @since 2.5.0 * @var string */ private $slug = 'algolia-account-seo'; /** * Admin page capabilities. * * @author WebDevStudios * @since 2.5.0 * @var string */ private $capability = 'manage_options'; /** * Admin page section. * * @author WebDevStudios * @since 2.5.0 * @var string */ private $section = 'algolia_section_seo'; /** * Admin page option group. * * @author WebDevStudios * @since 2.5.0 * @var string */ private $option_group = 'algolia_settings'; /** * The Algolia_Plugin instance. * * @author WebDevStudios * @since 2.5.0 * @var Algolia_Plugin */ private $plugin; /** * Algolia_Admin_Page_SEO constructor. * * @param Algolia_Plugin $plugin The Algolia_Plugin instance. * * @since 2.5.0 * @author WebDevStudios */ public function __construct( Algolia_Plugin $plugin ) { $this->plugin = $plugin; add_action( 'admin_menu', [ $this, 'add_page' ] ); add_action( 'admin_init', [ $this, 'add_settings' ] ); } /** * Add admin menu page. * * @return string|void The resulting page's hook_suffix. * @since 2.5.0 * @author WebDevStudios */ public function add_page() { $api = $this->plugin->get_api(); if ( ! $api->is_reachable() ) { return; } add_submenu_page( 'algolia', esc_html__( 'SEO', 'wp-search-with-algolia' ), sprintf( // translators: Placeholders are just for HTML markup that doesn't need translated. esc_html__( 'SEO %s', 'wp-search-with-algolia' ), sprintf( '%s', esc_html__( 'Pro', 'wp-search-with-algolia' ) ) ), $this->capability, $this->slug, [ $this, 'display_page' ] ); } /** * Add settings. * * @author WebDevStudios * @since 2.5.0 */ public function add_settings() { add_settings_section( $this->section, null, [ $this, 'print_section_settings' ], $this->slug ); } /** * Display the page. * * @author WebDevStudios * @since 2.5.0 */ public function display_page() { require_once dirname( __FILE__ ) . '/partials/form-options-seo.php'; } /** * Print the settings section. * * @author WebDevStudios * @since 2.5.0 */ public function print_section_settings() { } } ================================================ FILE: includes/admin/class-algolia-admin-page-settings.php ================================================ * @since 1.0.0 * * @package WebDevStudios\WPSWA */ /** * Class Algolia_Admin_Page_Settings * * @since 1.0.0 */ class Algolia_Admin_Page_Settings { /** * Admin page slug. * * @author WebDevStudios * @since 1.0.0 * * @var string */ private $slug = 'algolia-account-settings'; /** * Admin page capabilities. * * @author WebDevStudios * @since 1.0.0 * * @var string */ private $capability = 'manage_options'; /** * Admin page section. * * @author WebDevStudios * @since 1.0.0 * * @var string */ private $section = 'algolia_section_settings'; /** * Admin page option group. * * @author WebDevStudios * @since 1.0.0 * * @var string */ private $option_group = 'algolia_settings'; /** * The Algolia_Plugin instance. * * @author WebDevStudios * @since 1.0.0 * * @var Algolia_Plugin */ private $plugin; /** * Algolia_Admin_Page_Settings constructor. * * @author WebDevStudios * @since 1.0.0 * * @param Algolia_Plugin $plugin The Algolia_Plugin instance. */ public function __construct( Algolia_Plugin $plugin ) { $this->plugin = $plugin; add_action( 'admin_menu', array( $this, 'add_page' ) ); add_action( 'admin_init', array( $this, 'add_settings' ) ); add_action( 'admin_notices', array( $this, 'display_errors' ) ); // Display a link to this page from the plugins page. add_filter( 'plugin_action_links_' . ALGOLIA_PLUGIN_BASENAME, array( $this, 'add_action_links' ) ); } /** * Add action links. * * @author WebDevStudios * @since 1.0.0 * * @param array $links Array of action links. * * @return array */ public function add_action_links( array $links ) { return array_merge( $links, array( '' . esc_html__( 'Settings', 'wp-search-with-algolia' ) . '', ) ); } /** * Add admin menu page. * * @author WebDevStudios * @since 1.0.0 * * @return string|void The resulting page's hook_suffix. */ public function add_page() { $api = $this->plugin->get_api(); if ( ! $api->is_reachable() ) { // Means this is the only reachable admin page, so make it the default one! return add_menu_page( 'WP Search with Algolia', esc_html__( 'Algolia Search', 'wp-search-with-algolia' ), 'manage_options', $this->slug, array( $this, 'display_page' ), 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIGlkPSJMYXllcl8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MDAgNTAwLjM0Ij48ZGVmcz48c3R5bGU+LmNscy0xe2ZpbGw6IzAwM2RmZjt9PC9zdHlsZT48L2RlZnM+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMjUwLDBDMTEzLjM4LDAsMiwxMTAuMTYsLjAzLDI0Ni4zMmMtMiwxMzguMjksMTEwLjE5LDI1Mi44NywyNDguNDksMjUzLjY3LDQyLjcxLC4yNSw4My44NS0xMC4yLDEyMC4zOC0zMC4wNSwzLjU2LTEuOTMsNC4xMS02LjgzLDEuMDgtOS41MmwtMjMuMzktMjAuNzRjLTQuNzUtNC4yMi0xMS41Mi01LjQxLTE3LjM3LTIuOTItMjUuNSwxMC44NS01My4yMSwxNi4zOS04MS43NiwxNi4wNC0xMTEuNzUtMS4zNy0yMDIuMDQtOTQuMzUtMjAwLjI2LTIwNi4xLDEuNzYtMTEwLjMzLDkyLjA2LTE5OS41NSwyMDIuOC0xOTkuNTVoMjAyLjgzVjQwNy42OGwtMTE1LjA4LTEwMi4yNWMtMy43Mi0zLjMxLTkuNDMtMi42Ni0xMi40MywxLjMxLTE4LjQ3LDI0LjQ2LTQ4LjU2LDM5LjY3LTgxLjk4LDM3LjM2LTQ2LjM2LTMuMi04My45Mi00MC41Mi04Ny40LTg2Ljg2LTQuMTUtNTUuMjgsMzkuNjUtMTAxLjU4LDk0LjA3LTEwMS41OCw0OS4yMSwwLDg5Ljc0LDM3Ljg4LDkzLjk3LDg2LjAxLC4zOCw0LjI4LDIuMzEsOC4yOCw1LjUzLDExLjEzbDI5Ljk3LDI2LjU3YzMuNCwzLjAxLDguOCwxLjE3LDkuNjMtMy4zLDIuMTYtMTEuNTUsMi45Mi0yMy42LDIuMDctMzUuOTUtNC44My03MC4zOS02MS44NC0xMjcuMDEtMTMyLjI2LTEzMS4zNS04MC43My00Ljk4LTE0OC4yMyw1OC4xOC0xNTAuMzcsMTM3LjM1LTIuMDksNzcuMTUsNjEuMTIsMTQzLjY2LDEzOC4yOCwxNDUuMzYsMzIuMjEsLjcxLDYyLjA3LTkuNDIsODYuMi0yNi45N2wxNTAuMzYsMTMzLjI5YzYuNDUsNS43MSwxNi42MiwxLjE0LDE2LjYyLTcuNDhWOS40OUM1MDAsNC4yNSw0OTUuNzUsMCw0OTAuNTEsMEgyNTBaIi8+PC9zdmc+' ); } add_submenu_page( 'algolia', esc_html__( 'WP Search with Algolia Settings', 'wp-search-with-algolia' ), esc_html__( 'Settings', 'wp-search-with-algolia' ), $this->capability, $this->slug, array( $this, 'display_page' ), 0 ); } /** * Add settings. * * @author WebDevStudios * @since 1.0.0 */ public function add_settings() { add_settings_section( $this->section, null, array( $this, 'print_section_settings' ), $this->slug ); add_settings_field( 'algolia_application_id', esc_html__( 'Application ID', 'wp-search-with-algolia' ), array( $this, 'application_id_callback' ), $this->slug, $this->section ); add_settings_field( 'algolia_search_api_key', esc_html__( 'Search API key', 'wp-search-with-algolia' ), array( $this, 'search_api_key_callback' ), $this->slug, $this->section ); add_settings_field( 'algolia_api_key', esc_html__( 'Admin API key', 'wp-search-with-algolia' ), array( $this, 'api_key_callback' ), $this->slug, $this->section ); add_settings_field( 'algolia_index_name_prefix', esc_html__( 'Index name prefix', 'wp-search-with-algolia' ), array( $this, 'index_name_prefix_callback' ), $this->slug, $this->section ); add_settings_field( 'algolia_powered_by_enabled', esc_html__( 'Remove Algolia powered by logo', 'wp-search-with-algolia' ), array( $this, 'powered_by_enabled_callback' ), $this->slug, $this->section ); add_settings_field( 'algolia_insights_enabled', esc_html__( 'Enable Insight events', 'wp-search-with-algolia' ), array( $this, 'insights_enabled_callback' ), $this->slug, $this->section ); register_setting( $this->option_group, 'algolia_application_id', array( $this, 'sanitize_application_id' ) ); register_setting( $this->option_group, 'algolia_search_api_key', array( $this, 'sanitize_search_api_key' ) ); register_setting( $this->option_group, 'algolia_api_key', array( $this, 'sanitize_api_key' ) ); register_setting( $this->option_group, 'algolia_index_name_prefix', array( $this, 'sanitize_index_name_prefix' ) ); register_setting( $this->option_group, 'algolia_powered_by_enabled', array( $this, 'sanitize_powered_by_enabled' ) ); register_setting( $this->option_group, 'algolia_insights_enabled', array( $this, 'sanitize_insights_enabled' ) ); } /** * Application ID callback. * * @author WebDevStudios * @since 1.0.0 */ public function application_id_callback() { $settings = $this->plugin->get_settings(); $setting = $settings->get_application_id(); $disabled_html = $settings->is_application_id_in_config() ? ' disabled' : ''; ?> />

* @since 1.0.0 */ public function search_api_key_callback() { $settings = $this->plugin->get_settings(); $setting = $settings->get_search_api_key(); $disabled_html = $settings->is_search_api_key_in_config() ? ' disabled' : ''; ?> />

* @since 1.0.0 */ public function api_key_callback() { $settings = $this->plugin->get_settings(); $setting = $settings->get_api_key(); $disabled_html = $settings->is_api_key_in_config() ? ' disabled' : ''; ?> />

* @since 1.0.0 */ public function index_name_prefix_callback() { $settings = $this->plugin->get_settings(); $index_name_prefix = $settings->get_index_name_prefix(); $disabled_html = $settings->is_index_name_prefix_in_config() ? ' disabled' : ''; ?> />

* @since 2020-07-24 */ public function powered_by_enabled_callback() { $powered_by_enabled = $this->plugin->get_settings()->is_powered_by_enabled(); $checked = ''; if ( ! $powered_by_enabled ) { $checked = ' checked'; } echo "' . '

' . esc_html( __( 'This will remove the Algolia logo from the autocomplete and the search page. Algolia requires that you keep the logo if you are using a free plan.', 'wp-search-with-algolia' ) ) . '

'; } /** * Insights enabled callback. * * @since 2.10.2 */ public function insights_enabled_callback() { $insights_enabled = $this->plugin->get_settings()->is_insights_enabled(); $checked = ''; if ( $insights_enabled ) { $checked = ' checked'; } echo "' . '

' . esc_html( __( 'This will enable insights and events tracking to help boost your Algolia results.', 'wp-search-with-algolia' ) ) . '

'; } /** * Sanitize application ID. * * @author Richard Aber * @since 2020-07-24 * * @param string $value The value to sanitize. * * @return string */ public function sanitize_application_id( $value ) { if ( $this->plugin->get_settings()->is_application_id_in_config() ) { $value = $this->plugin->get_settings()->get_application_id(); } $value = sanitize_text_field( $value ); if ( empty( $value ) ) { add_settings_error( $this->option_group, 'empty', esc_html__( 'Application ID should not be empty.', 'wp-search-with-algolia' ) ); } return $value; } /** * Sanitize search API key. * * @author Richard Aber * @since 2020-07-24 * * @param string $value The value to sanitize. * * @return string */ public function sanitize_search_api_key( $value ) { if ( $this->plugin->get_settings()->is_search_api_key_in_config() ) { $value = $this->plugin->get_settings()->get_search_api_key(); } $value = sanitize_text_field( $value ); if ( empty( $value ) ) { add_settings_error( $this->option_group, 'empty', esc_html__( 'Search API key should not be empty.', 'wp-search-with-algolia' ) ); } return $value; } /** * Sanitize Admin API key. * * @author Richard Aber * @since 2020-07-24 * * @param string $value The value to sanitize. * * @return string */ public function sanitize_api_key( $value ) { if ( $this->plugin->get_settings()->is_api_key_in_config() ) { $value = $this->plugin->get_settings()->get_api_key(); } $value = sanitize_text_field( $value ); if ( empty( $value ) ) { add_settings_error( $this->option_group, 'empty', esc_html__( 'API key should not be empty', 'wp-search-with-algolia' ) ); } $errors = get_settings_errors( $this->option_group ); // @todo Not 100% clear why this is returning here. if ( ! empty( $errors ) ) { return $value; } $settings = $this->plugin->get_settings(); $valid_credentials = true; try { Algolia_API::assert_valid_credentials( $settings->get_application_id(), $value ); } catch ( Exception $exception ) { $valid_credentials = false; add_settings_error( $this->option_group, 'login_exception', $exception->getMessage() ); } if ( ! $valid_credentials ) { add_settings_error( $this->option_group, 'no_connection', esc_html__( 'We were unable to authenticate you against the Algolia servers with the provided information. Please ensure that you used a valid Application ID and Admin API key.', 'wp-search-with-algolia' ) ); $settings->set_api_is_reachable( false ); } else { if ( ! Algolia_API::is_valid_search_api_key( $settings->get_application_id(), $settings->get_search_api_key() ) ) { add_settings_error( $this->option_group, 'wrong_search_API_key', esc_html__( 'It looks like your search API key is wrong. Ensure that the key you entered has only the search capability and nothing else. Also ensure that the key has no limited time validity.', 'wp-search-with-algolia' ) ); $settings->set_api_is_reachable( false ); } else { add_settings_error( $this->option_group, 'connection_success', esc_html__( 'Connection to the Algolia servers was succesful! Configure your Search Page to start using Algolia!', 'wp-search-with-algolia' ), 'updated' ); $settings->set_api_is_reachable( true ); } } return $value; } /** * Determine if the index name prefix is valid. * * @author WebDevStudios * @since 1.0.0 * * @param string $index_name_prefix The index name prefix. * * @return bool */ public function is_valid_index_name_prefix( $index_name_prefix ) { $to_validate = str_replace( '_', '', $index_name_prefix ); return ctype_alnum( $to_validate ); } /** * Sanitize the index name prefix. * * @author WebDevStudios * @since 1.0.0 * * @param string $value The value to sanitize. * * @return bool|mixed|string|void */ public function sanitize_index_name_prefix( $value ) { if ( $this->plugin->get_settings()->is_index_name_prefix_in_config() ) { $value = $this->plugin->get_settings()->get_index_name_prefix(); } if ( $this->is_valid_index_name_prefix( $value ) ) { return $value; } add_settings_error( $this->option_group, 'wrong_prefix', esc_html__( 'Indices prefix can only contain alphanumeric characters and underscores.', 'wp-search-with-algolia' ) ); $value = get_option( 'algolia_index_name_prefix' ); return $this->is_valid_index_name_prefix( $value ) ? $value : 'wp_'; } /** * Sanitize the powered by enabled setting. * * @author WebDevStudios * @since 1.0.0 * * @param string $value The value to sanitize. * * @return string */ public function sanitize_powered_by_enabled( $value ) { return 'no' === $value ? 'no' : 'yes'; } /** * Sanitize the insights enabled setting. * * @since 2.10.2 * * @param string $value The value to sanitize. * * @return string */ public function sanitize_insights_enabled( $value ) { return 'yes' === $value ? 'yes' : 'no'; } /** * Display the page. * * @author WebDevStudios * @since 1.0.0 */ public function display_page() { require_once dirname( __FILE__ ) . '/partials/form-options.php'; } /** * Display errors. * * @author WebDevStudios * @since 1.0.0 */ public function display_errors() { settings_errors( $this->option_group ); } /** * Print the settings section. * * @author WebDevStudios * @since 1.0.0 */ public function print_section_settings() { echo '

' . wp_kses( sprintf( // translators: URL to API keys section in Algolia dashboard. __( 'Configure your Algolia account credentials. You can find them in the API Keys section of your Algolia dashboard.', 'wp-search-with-algolia' ), 'https://dashboard.algolia.com/account/api-keys/all' ), [ 'a' => [ 'href' => [], 'target' => [], ], ] ) . '

'; // translators: the placeholder contains the URL to Algolia's website. echo '

' . wp_kses_post( sprintf( __( 'No Algolia account yet? Follow this link to create one for free in a couple of minutes!', 'wp-search-with-algolia' ), 'https://dashboard.algolia.com/users/sign_up' ) ) . '

'; echo '

' . esc_html__( 'Once you provide your Algolia Application ID and API key, this plugin will be able to securely communicate with Algolia servers.', 'wp-search-with-algolia' ) . '
' . esc_html__( 'We ensure your information is correct by testing them against the Algolia servers upon save.', 'wp-search-with-algolia' ) . '

'; ?> * @since 2.5.0 * @package WebDevStudios\WPSWA */ /** * Class Algolia_Admin_Page_WooCommerce * * @since 2.5.0 */ class Algolia_Admin_Page_WooCommerce { /** * Admin page slug. * * @author WebDevStudios * @since 2.5.0 * @var string */ private $slug = 'algolia-account-woocommerce'; /** * Admin page capabilities. * * @author WebDevStudios * @since 2.5.0 * @var string */ private $capability = 'manage_options'; /** * Admin page section. * * @author WebDevStudios * @since 2.5.0 * @var string */ private $section = 'algolia_section_woocommerce'; /** * Admin page option group. * * @author WebDevStudios * @since 2.5.0 * @var string */ private $option_group = 'algolia_settings'; /** * The Algolia_Plugin instance. * * @author WebDevStudios * @since 2.5.0 * @var Algolia_Plugin */ private $plugin; /** * Algolia_Admin_Page_WooCommerce constructor. * * @param Algolia_Plugin $plugin The Algolia_Plugin instance. * * @since 2.5.0 * @author WebDevStudios */ public function __construct( Algolia_Plugin $plugin ) { $this->plugin = $plugin; add_action( 'admin_menu', [ $this, 'add_page' ] ); add_action( 'admin_init', [ $this, 'add_settings' ] ); } /** * Add admin menu page. * * @return string|void The resulting page's hook_suffix. * @since 2.5.0 * @author WebDevStudios */ public function add_page() { $api = $this->plugin->get_api(); if ( ! $api->is_reachable() ) { return; } add_submenu_page( 'algolia', esc_html__( 'WooCommerce', 'wp-search-with-algolia' ), sprintf( // translators: Placeholders are just for HTML markup that doesn't need translated. esc_html__( 'WooCommerce %s', 'wp-search-with-algolia' ), sprintf( '%s', esc_html__( 'Pro', 'wp-search-with-algolia' ) ) ), $this->capability, $this->slug, [ $this, 'display_page' ] ); } /** * Add settings. * * @author WebDevStudios * @since 2.5.0 */ public function add_settings() { add_settings_section( $this->section, null, [ $this, 'print_section_settings' ], $this->slug ); } /** * Display the page. * * @author WebDevStudios * @since 2.5.0 */ public function display_page() { require_once dirname( __FILE__ ) . '/partials/form-options-woocommerce.php'; } /** * Print the settings section. * * @author WebDevStudios * @since 2.5.0 */ public function print_section_settings() { } } ================================================ FILE: includes/admin/class-algolia-admin-template-notices.php ================================================ * @since 1.8.0 * * @package WebDevStudios\WPSWA */ /** * Class Algolia_Admin_Template_Notices * * @since 1.8.0 */ class Algolia_Admin_Template_Notices { /** * Algolia_Admin_Template_Notices constructor. * * @author WebDevStudios * @since 1.8.0 */ public function __construct() { add_action( 'admin_notices', [ $this, 'template_version_notices' ] ); } /** * Display template version discrepencany notices. * * @author WebDevStudios * @since 1.8.0 * * @return void */ public function template_version_notices() { $core_template_paths = Algolia_Template_Utils::get_core_template_paths(); $custom_template_paths = Algolia_Template_Utils::get_custom_template_paths(); if ( empty( $custom_template_paths ) ) { return; } $core_template_versions = []; $custom_template_versions = []; foreach ( $custom_template_paths as $filename => $file_path ) { $core_template_versions[ $filename ] = Algolia_Template_Utils::get_template_version( $core_template_paths[ $filename ] ); $custom_template_versions[ $filename ] = Algolia_Template_Utils::get_template_version( $file_path ); } foreach ( $custom_template_versions as $filename => $file_version ) { // Error if versions do not match, or custom template version unknown. if ( version_compare( $file_version, $core_template_versions[ $filename ], '!=' ) ) { $error_notices[] = sprintf( // translators: placeholder 1 is template filename, placeholder 2 is custom template version, placeholder 3 is core template version. esc_html__( 'Your custom WP Search With Algolia template file, %1$s, version %2$s is out of date. The core version is %3$s', 'wp-search-with-algolia' ), $filename, ! empty( $file_version ) ? $file_version : __( 'unknown', 'wp-search-with-algolia' ), $core_template_versions[ $filename ] ); } } if ( empty( $error_notices ) ) { return; } foreach ( $error_notices as $error_notice ) { echo '

' . esc_html( $error_notice ) . '

'; } } } ================================================ FILE: includes/admin/class-algolia-admin.php ================================================ * @since 1.0.0 * * @package WebDevStudios\WPSWA */ // phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber -- We're using RuntimeException. /** * Class Algolia_Admin * * @since 1.0.0 */ class Algolia_Admin { /** * The Algolia Plugin. * * @since 1.0.0 * * @var Algolia_Plugin */ private $plugin; /** * Algolia_Admin constructor. * * @author WebDevStudios * @since 1.0.0 * * @param Algolia_Plugin $plugin The Algolia Plugin. */ public function __construct( Algolia_Plugin $plugin ) { $this->plugin = $plugin; add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_styles' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'localize_scripts' ) ); $api = $plugin->get_api(); if ( $api->is_reachable() ) { new Algolia_Admin_Page_Autocomplete( $plugin->get_settings(), $this->plugin->get_autocomplete_config() ); new Algolia_Admin_Page_Native_Search( $plugin ); add_action( 'wp_ajax_algolia_re_index', array( $this, 're_index' ) ); add_action( 'wp_ajax_algolia_push_settings', array( $this, 'push_settings' ) ); $maybe_get_page = filter_input( INPUT_GET, 'page', FILTER_SANITIZE_SPECIAL_CHARS ); if ( ! empty( $maybe_get_page ) && 'algolia' === substr( $maybe_get_page, 0, 7 ) ) { add_action( 'admin_notices', array( $this, 'display_reindexing_notices' ) ); } } new Algolia_Admin_Template_Notices(); new Algolia_Admin_Page_Settings( $plugin ); new Algolia_Admin_Page_WooCommerce( $plugin ); new Algolia_Admin_Page_SEO( $plugin ); new Algolia_Admin_Page_Premium_Support( $plugin ); add_action( 'admin_notices', array( $this, 'display_unmet_requirements_notices' ) ); add_filter( 'admin_footer_text', array( $this, 'algolia_footer' ) ); add_action( 'admin_menu', [ $this, 'add_pro_menu_item' ], 1000 ); add_action( 'admin_init', [ $this, 'handle_pro_redirect' ] ); } /** * Enqueue styles. * * @author WebDevStudios * @since 1.0.0 */ public function enqueue_styles() { wp_enqueue_style( 'algolia-admin', plugin_dir_url( __FILE__ ) . 'css/algolia-admin.css', array(), ALGOLIA_VERSION ); } /** * Enqueue scripts. * * @author WebDevStudios * @since 1.0.0 */ public function enqueue_scripts() { wp_enqueue_script( 'algolia-admin', plugin_dir_url( __FILE__ ) . 'js/algolia-admin.js', array( 'jquery', 'jquery-ui-sortable' ), ALGOLIA_VERSION, false ); wp_enqueue_script( 'algolia-admin-reindex-button', plugin_dir_url( __FILE__ ) . 'js/reindex-button.js', array( 'jquery' ), ALGOLIA_VERSION, false ); wp_enqueue_script( 'algolia-admin-push-settings-button', plugin_dir_url( __FILE__ ) . 'js/push-settings-button.js', array( 'jquery' ), ALGOLIA_VERSION, false ); } /** * Add localize strings to scripts. * * @author WebDevStudios * @since 2.2.0 */ public function localize_scripts() { wp_localize_script( 'algolia-admin-push-settings-button', 'algoliaPushSettingsButton', array( 'pushBtnAlert' => esc_html__( 'Warning: Pushing settings will override the settings in the Algolia dashboard. Do you want to continue?', 'wp-search-with-algolia' ), ) ); } /** * Displays an error notice for every unmet requirement. * * @author WebDevStudios * @since 1.0.0 * * @return void */ public function display_unmet_requirements_notices() { if ( ! extension_loaded( 'mbstring' ) ) { echo '

' . esc_html__( 'Algolia Search requires the "mbstring" PHP extension to be enabled. Please contact your hosting provider.', 'wp-search-with-algolia' ) . '

'; } elseif ( ! function_exists( 'mb_ereg_replace' ) ) { echo '

' . esc_html__( 'Algolia needs "mbregex" NOT to be disabled. Please contact your hosting provider.', 'wp-search-with-algolia' ) . '

'; } if ( ! extension_loaded( 'curl' ) ) { echo '

' . esc_html__( 'Algolia Search requires the "cURL" PHP extension to be enabled. Please contact your hosting provider.', 'wp-search-with-algolia' ) . '

'; return; } $this->w3tc_notice(); } /** * Display notice to help users adding 'algolia_' as an ignored query string to the db caching configuration. * * @author WebDevStudios * @since 1.0.0 * * @return void */ public function w3tc_notice() { if ( ! function_exists( 'w3tc_pgcache_flush' ) || ! function_exists( 'w3_instance' ) ) { return; } $config = w3_instance( 'W3_Config' ); $enabled = $config->get_integer( 'dbcache.enabled' ); $settings = array_map( 'trim', $config->get_array( 'dbcache.reject.sql' ) ); if ( $enabled && ! in_array( 'algolia_', $settings, true ) ) { // translators: placeholder contains the URL to the caching plugin's config page. $message = sprintf( __( 'In order for database caching to work with Algolia you must add algolia_ to the "Ignored Query Stems" option in W3 Total Cache settings here.', 'wp-search-with-algolia' ), esc_url( admin_url( 'admin.php?page=w3tc_dbcache' ) ) ); ?>

* @since 1.0.0 */ public function display_reindexing_notices() { $indices = $this->plugin->get_indices( array( 'enabled' => true, ) ); $allowed_html = array( 'strong' => array(), ); foreach ( $indices as $index ) { if ( $index->exists() ) { continue; } ?>

%1$s', 'wp-search-with-algolia' ), esc_html( $index->get_admin_name() ) ), $allowed_html ); ?>

* @since 1.0.0 * * @throws RuntimeException If index ID or page are not provided, or index name dies not exist. * @throws Exception If index ID or page are not provided, or index name dies not exist. */ public function re_index() { $index_id = filter_input( INPUT_POST, 'index_id', FILTER_SANITIZE_SPECIAL_CHARS ); $page = filter_input( INPUT_POST, 'p', FILTER_SANITIZE_SPECIAL_CHARS ); try { if ( empty( $index_id ) ) { throw new RuntimeException( 'Index ID should be provided.' ); } if ( ! ctype_digit( $page ) ) { throw new RuntimeException( 'Page should be provided.' ); } $page = (int) $page; $index = $this->plugin->get_index( $index_id ); if ( null === $index ) { throw new RuntimeException( sprintf( 'Index named %s does not exist.', $index_id ) ); } $total_pages = $index->get_re_index_max_num_pages(); ob_start(); if ( $page <= $total_pages || 0 === $total_pages ) { $index->re_index( $page ); } ob_end_clean(); $response = array( 'totalPagesCount' => $total_pages, 'finished' => $page >= $total_pages, ); wp_send_json( $response ); } catch ( Exception $exception ) { wp_send_json_error( array( 'message' => $exception->getMessage() ) ); } } /** * Push settings. * * @author WebDevStudios * @since 1.0.0 * * @throws RuntimeException If index_id is not provided or if the corresponding index is null. * @throws Exception If index_id is not provided or if the corresponding index is null. */ public function push_settings() { $index_id = filter_input( INPUT_POST, 'index_id', FILTER_SANITIZE_SPECIAL_CHARS ); try { if ( empty( $index_id ) ) { throw new RuntimeException( 'index_id should be provided.' ); } $index = $this->plugin->get_index( $index_id ); if ( null === $index ) { throw new RuntimeException( sprintf( 'Index named %s does not exist.', $index_id ) ); } $index->push_settings(); $response = array( 'success' => true, ); wp_send_json( $response ); } catch ( Exception $exception ) { wp_send_json_error( array( 'message' => $exception->getMessage() ) ); } } /** * Display footer links and plugin credits. * * @since 0.3.0 * * @internal * * @param string $original Original footer content. Optional. Default empty string. * @return string $value HTML for footer. */ public function algolia_footer( $original = '' ) { $screen = get_current_screen(); if ( ! is_object( $screen ) || 'algolia' !== $screen->parent_base ) { return $original; } return sprintf( // translators: Placeholder will hold the name of the plugin, version of the plugin and a link to WebdevStudios. esc_attr__( '%1$s version %2$s by %3$s', 'wp-search-with-algolia' ), esc_attr__( 'WP Search with Algolia', 'wp-search-with-algolia' ), ALGOLIA_VERSION, 'WebDevStudios' ) . ' - ' . sprintf( // translators: Placeholders are just for HTML markup that doesn't need translated. '%s', esc_attr__( 'Support', 'wp-search-with-algolia' ) ) . ' - ' . sprintf( // translators: Placeholders are just for HTML markup that doesn't need translated. '%s', esc_attr__( 'Review', 'wp-search-with-algolia' ) ) . ' - ' . sprintf( // translators: Placeholders are just for HTML markup that doesn't need translated. '%s', esc_attr__( 'Go Pro', 'wp-search-with-algolia' ) ) . ' - ' . esc_attr__( 'Follow on X:', 'wp-search-with-algolia' ) . sprintf( // translators: Placeholders are just for HTML markup that doesn't need translated. ' %s', 'WebDevStudios' ); } /** * Add an "Upgrade to Pro" submenu link. * * @internal * * @since 2.5.0 */ public function add_pro_menu_item() { global $submenu; $submenu['algolia'][] = [ // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Only real way to modify in this way. '' . esc_html__( 'Upgrade to Pro', 'wp-search-with-algolia' ) . '', 'manage_options', wp_nonce_url( add_query_arg( [ 'page' => 'algolia-account-settings', 'algolia-pro-upgrade' => wp_create_nonce( 'algolia-pro-nonce' ), ], admin_url( 'admin.php' ) ) ), ]; } /** * Handle redirect to purchase WP Search with Algolia Pro link click. * * @since 2.5.0 */ public function handle_pro_redirect() { if ( isset( $_GET['algolia-pro-upgrade'] ) && wp_verify_nonce( $_GET['algolia-pro-upgrade'], 'algolia-pro-nonce' ) ) { wp_redirect( 'https://pluginize.com/plugins/wp-search-with-algolia-pro/' ); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect exit(); } } } // phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber -- We're using RuntimeException. ================================================ FILE: includes/admin/css/algolia-admin.css ================================================ /** * All of the CSS for your admin-specific functionality should be * included in this file. */ /* * https://algolia.frontify.com/document/1?#/basics/colors */ :root { --algolia-white: #fff; --algolia-gray: #f5f5fa; --algolia-blue: #003dff; --algolia-blue-lighten: #07f; --algolia-dark-blue: #003; --algolia-neon: #ceff00; } .form-table .table-autocomplete th { padding: 8px 10px !important; } .form-table .table-autocomplete { width: auto; } .table-autocomplete .dashicons-move { cursor: pointer; } /* Native search page */ form .input-radio label { font-weight: bold; } form .radio-info { margin-bottom: 15px; padding-left: 25px; } #toplevel_page_algolia .algolia-pro-indicator { background-color: var(--algolia-blue); border-radius: 3px; color: var(--algolia-white); font-size: 10px; padding: 2px 3px; vertical-align: text-top; } .algolia-menu-highlight { color: var(--algolia-white); } #toplevel_page_algolia .algolia-submenu-highlight { background-color: var(--algolia-blue); font-weight: bold; } .algolia-pro-cta { background-color: var(--algolia-white); left: 50%; margin-top: 0; max-width: 850px; padding: 30px 20px 40px; position: absolute; text-align: center; top: 50px; transform: translate(-50%) translateY(0); width: 100%; } .algolia-pro-cta .algolia-pro-title { color: #41495b; font-size: 46px; font-weight: 800; line-height: 58px; margin: 0 auto; max-width: 600px; } .algolia-pro-cta .algolia-pro-desc { color: #41495b; font-size: 22px; font-weight: 500; line-height: 27px; margin: 20px auto 20px; max-width: 560px; } .algolia-pro-cta .algolia-pro-button { background-color: var(--algolia-blue); border-bottom: 0; border-radius: 4px; color: var(--algolia-white); cursor: pointer; display: inline-block; font-size: 24px; font-weight: 700; line-height: 26px; padding: 15px 40px; text-decoration: none; } .algolia-pro-cta .algolia-pro-button:hover { background-color: var(--algolia-blue-lighten); } .algolia-pro-cta .algolia-pro-more { display: block; font-size: 14px; margin-top: 15px; position: relative; text-align: center; } .algolia-pro-cta .algolia-pro-more a { color: var(--algolia-blue); display: inline-block; font-size: 14px; font-style: italic; outline: none; text-decoration: none; } .algolia-pro-cta .algolia-pro-more a:hover { color: var(--algolia-blue-lighten); } .algolia-pro-cta .algolia-pro-features { display: flex; gap: 40px; justify-content: center; margin: 20px auto 40px; text-align: left; } .algolia-pro-cta .algolia-pro-features h4 { font-size: 18px; margin: 12px; } .algolia-pro-cta .algolia-pro-feature { align-items: center; display: flex; } .algolia-pro-cta .algolia-pro-feature svg { padding-right: 4px; } .algolia-premium-wrap-block { display: flex; flex-direction: column; gap: 20px; justify-content: space-between; } .algolia-premium-support-block { text-align: center; } .algolia-premium-support-block.algolia-pro-block { text-align: left; } .wds-premium { background: var(--algolia-blue); color: var(--algolia-white); padding: 10px; border-radius: 9999px; text-decoration: none; } .wds-premium:hover { color: var(--algolia-white); } .algolia-flex { align-items: center; display: flex; flex-wrap: wrap; justify-content: center; } .algolia-flex-item { padding: 5px 10px; } .algolia-pro-flex-wrap { display: flex; flex-wrap: wrap; justify-content: space-between; align-content: space-between; gap: 10px; } .algolia-pro-flex-item { border-color: var(--algolia-blue); border-radius: 15px; flex: 1; width: 33%; } .algolia-pro-features { padding-left: 15px; } .algolia-pro-features li { list-style: disc; } ================================================ FILE: includes/admin/css/index.php ================================================ { let link = child.querySelector('a') if (!link) { return } let linkChild = link.querySelector('.algolia-menu-highlight') if (linkChild) { child.classList.add('algolia-submenu-highlight') let link = child.querySelector('a') if (link) { link.setAttribute('target', '_blank') } } }) } submenuHighlight(); } ); })( jQuery ); ================================================ FILE: includes/admin/js/index.php ================================================ 0) { return 'If you leave now, re-indexing tasks in progress will be aborted'; } } ); function handleReindexButtonClick(e) { $clickedButton = $( e.currentTarget ); var index = $clickedButton.data( 'index' ); if ( ! index) { throw new Error( 'Clicked button has no "data-index" set.' ); } ongoing++; $clickedButton.attr( 'disabled', 'disabled' ); $clickedButton.data( 'originalText', $clickedButton.text() ); updateIndexingPourcentage( $clickedButton, 0 ); reIndex( $clickedButton, index ); } function updateIndexingPourcentage($clickedButton, amount) { $clickedButton.text( 'Processing, please be patient ... ' + amount + '%' ); } function reIndex($clickedButton, index, currentPage) { if ( ! currentPage) { currentPage = 1; } var data = { 'action': 'algolia_re_index', 'index_id': index, 'p': currentPage }; $.post( ajaxurl, data, function(response) { if (typeof response.totalPagesCount === 'undefined') { alert( 'An error occurred' ); resetButton( $clickedButton ); return; } if (response.totalPagesCount === 0) { $clickedButton.parents( '.error' ).fadeOut(); resetButton( $clickedButton ); return; } progress = Math.round( (currentPage / response.totalPagesCount) * 100 ); updateIndexingPourcentage( $clickedButton, progress ); if (response.finished !== true) { reIndex( $clickedButton, index, ++currentPage ); } else { $clickedButton.parents( '.error' ).fadeOut(); resetButton( $clickedButton ); } } ).fail( function(response) { alert( 'An error occurred: ' + response.responseText ); resetButton( $clickedButton ); } ); } function resetButton($clickedButton) { ongoing--; $clickedButton.text( $clickedButton.data( 'originalText' ) ); $clickedButton.removeAttr( 'disabled' ); $clickedButton.data( 'currentPage', 1 ); } })( jQuery ); ================================================ FILE: includes/admin/partials/form-options-premium-support.php ================================================ * @since 2.5.0 * @package WebDevStudios\WPSWA */ if ( ! defined( 'ABSPATH' ) ) { exit; } ?>

  • '

================================================ FILE: includes/admin/partials/form-options-seo.php ================================================ * @since 2.5.0 * @package WebDevStudios\WPSWA */ ?>

<?php esc_attr_e( 'Blurry representiation of features available with WP Search with Algolia Pro.', 'wp-search-with-algolia' ); ?>
================================================ FILE: includes/admin/partials/form-options-woocommerce.php ================================================ * @since 2.5.0 * @package WebDevStudios\WPSWA */ ?>

<?php esc_attr_e( 'Blurry representiation of features available with WP Search with Algolia Pro.', 'wp-search-with-algolia' ); ?>
================================================ FILE: includes/admin/partials/form-options.php ================================================ * @since 1.0.0 * * @package WebDevStudios\WPSWA */ ?>

option_group ); do_settings_sections( $this->slug ); submit_button(); ?>
================================================ FILE: includes/admin/partials/form-override-search-option.php ================================================ * @since 1.0.0 * * @package WebDevStudios\WPSWA */ ?>
[], ] ); ?>
This option has the advantage to play nicely with any theme but does not support filtering and displaying InstantSearch results.', 'wp-search-with-algolia' ), [ 'br' => [], 'b' => [], ] ); ?>
By default you will be able to filter by post type, categories, tags and authors.', 'wp-search-with-algolia' ), [ 'br' => [], ] ); ?>
================================================ FILE: includes/admin/partials/form-override-search-version-option.php ================================================ * @since 2.9.0 * * @package WebDevStudios\WPSWA */ ?>
[], ] ); ?>
[], ] ); ?>

================================================ FILE: includes/admin/partials/index.php ================================================ * @since 1.0.0 * * @package WebDevStudios\WPSWA */ ?>



settings->get_index_name_prefix(); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound foreach ( $indices as $index ) : // phpcs:ignore -- This is an admin partial. ?>
/>

0 ) : ?>



================================================ FILE: includes/admin/partials/page-autocomplete.php ================================================ * @since 1.0.0 * * @package WebDevStudios\WPSWA */ ?>

option_group ); do_settings_sections( $this->slug ); submit_button(); ?>
================================================ FILE: includes/admin/partials/page-search.php ================================================ * @since 1.0.0 * * @package WebDevStudios\WPSWA */ ?>

option_group ); do_settings_sections( $this->slug ); submit_button(); ?>
================================================ FILE: includes/class-algolia-api.php ================================================ * @since 1.0.0 * * @package WebDevStudios\WPSWA */ use WebDevStudios\WPSWA\Algolia\AlgoliaSearch\Exceptions\AlgoliaException; use WebDevStudios\WPSWA\Algolia\AlgoliaSearch\SearchClient; /** * Class Algolia_API * * @since 1.0.0 */ class Algolia_API { /** * The SearchClient instance. * * @author WebDevStudios * @since 1.0.0 * * @var SearchClient */ private $client; /** * The Algolia_Settings instance. * * @author WebDevStudios * @since 1.0.0 * * @var Algolia_Settings */ private $settings; /** * Algolia_API constructor. * * @author WebDevStudios * @since 1.0.0 * * @param Algolia_Settings $settings The Algolia_Settings instance. */ public function __construct( Algolia_Settings $settings ) { $this->settings = $settings; } /** * Check if the Aloglia API is reachable. * * @author WebDevStudios * @since 1.0.0 * * @return bool */ public function is_reachable() { if ( ! $this->settings->get_api_is_reachable() ) { return false; } try { // Here we check that all requirements for the PHP API SearchClient are met. // If they are not, instantiating the client will throw exceptions. $client = $this->get_client(); } catch ( Exception $e ) { return false; } return null !== $client; } /** * Get the SearchClient. * * @author WebDevStudios * @since 1.0.0 * * @return SearchClient|null */ public function get_client(): ?SearchClient { $application_id = $this->settings->get_application_id(); $api_key = $this->settings->get_api_key(); if ( empty( $application_id ) || empty( $api_key ) ) { return null; } if ( null === $this->client ) { $this->client = Algolia_Search_Client_Factory::create( (string) $this->settings->get_application_id(), (string) $this->settings->get_api_key() ); } return $this->client; } /** * Assert that the credentials are valid. * * @author WebDevStudios * @since 1.0.0 * * @param string $application_id The Algolia Application ID. * @param string $api_key The Algolia Admin API Key. * * @return void * * @throws Exception If the Algolia Admin API Key does not have correct ACLs. */ public static function assert_valid_credentials( $application_id, $api_key ) { $client = Algolia_Search_Client_Factory::create( (string) $application_id, (string) $api_key ); // This checks if the API Key is an Admin API key. // Admin API keys have no scopes so we need a separate check here. try { $client->listApiKeys(); return; } catch ( Exception $exception ) { // phpcs:ignore --- intentionally empty catch. } // If this call does not succeed, then the application_ID or API_key is/are wrong. // This will raise an exception. $key = $client->getApiKey( (string) $api_key ); $required_acls = array( 'addObject', 'deleteObject', 'listIndexes', 'deleteIndex', 'settings', 'editSettings', ); $missing_acls = array(); foreach ( $required_acls as $required_acl ) { if ( ! in_array( $required_acl, $key['acl'], true ) ) { $missing_acls[] = $required_acl; } } if ( ! empty( $missing_acls ) ) { throw new Exception( 'Your admin API key is missing the following ACLs: ' . implode( ', ', $missing_acls ) ); } } /** * Check if the credentials are valid. * * @author WebDevStudios * @since 1.0.0 * * @param string $application_id The Algolia Application ID. * @param string $api_key The Algolia Admin API Key. * * @return bool */ public static function is_valid_credentials( $application_id, $api_key ) { try { self::assert_valid_credentials( $application_id, $api_key ); } catch ( Exception $e ) { return false; } return true; } /** * Check if the Search API Key is valid. * * @author WebDevStudios * @since 1.0.0 * * @param string $application_id The Algolia Application ID. * @param string $search_api_key The Algolia Search API Key. * * @return bool */ public static function is_valid_search_api_key( $application_id, $search_api_key ) { $client = Algolia_Search_Client_Factory::create( (string) $application_id, (string) $search_api_key ); // If this call does not succeed, the application_ID and/or API_key are wrong. try { $acl = $client->getApiKey( $search_api_key ); } catch ( AlgoliaException $e ) { return false; } // We expect a search only key for security reasons. Will be used in front. $scopes = array_flip( $acl['acl'] ); if ( ! isset( $scopes['search'] ) ) { return false; } unset( $scopes['search'] ); if ( isset( $scopes['settings'] ) ) { unset( $scopes['settings'] ); } if ( isset( $scopes['listIndexes'] ) ) { unset( $scopes['listIndexes'] ); } if ( isset( $scopes['browse'] ) ) { unset( $scopes['browse'] ); } // Short circuit ACL checks for local development. if ( defined( 'WP_LOCAL_DEV' ) && WP_LOCAL_DEV ) { return true; } if ( ! empty( $scopes ) ) { // The API key has more permissions than allowed. return false; } // We do expect a search key without unlimited TTL. if ( 0 !== $acl['validity'] ) { return false; } return true; } } ================================================ FILE: includes/class-algolia-autocomplete-config.php ================================================ * @since 1.0.0 * * @package WebDevStudios\WPSWA */ /** * Class Algolia_Autocomplete_Config * * @since 1.0.0 */ class Algolia_Autocomplete_Config { /** * The Algolia_Plugin instance. * * @author WebDevStudios * @since 1.0.0 * * @var Algolia_Plugin */ private $plugin; /** * Algolia_Autocomplete_Config constructor. * * @author WebDevStudios * @since 1.0.0 * * @param Algolia_Plugin $plugin The Algolia_Plugin instance. */ public function __construct( Algolia_Plugin $plugin ) { $this->plugin = $plugin; } /** * Get form data. * * @author WebDevStudios * @since 1.0.0 * * @return array */ public function get_form_data() { $indices = $this->plugin->get_indices(); $config = array(); $existing_config = $this->get_config(); /** * Loop over the indices. * * @author WebDevStudios * @since 1.0.0 * * @var Algolia_Index $index */ foreach ( $indices as $index ) { $index_config = $this->extract_index_config( $existing_config, $index->get_id() ); if ( $index_config ) { // If there is an existing configuration, add it. $config[] = $index_config; continue; } $default_config = $index->get_default_autocomplete_config(); $default_config['enabled'] = false; $config[] = $default_config; } usort( $config, function( $a, $b ):int { return $a['position'] <=> $b['position']; } ); return $config; } /** * Sanitize form data. * * @author WebDevStudios * @since 1.0.0 * * @param array $data The data to sanitize. * * @return mixed */ public function sanitize_form_data( $data ) { if ( ! is_array( $data ) ) { return array(); } $sanitized = array(); foreach ( $data as $index_id => $config ) { $index = $this->plugin->get_index( $index_id ); // Remove disabled indices. if ( ! isset( $config['enabled'] ) ) { continue; } $merged_config = array_merge( $index->get_default_autocomplete_config(), array( 'position' => (int) $config['position'], 'max_suggestions' => (int) $config['max_suggestions'], ) ); if ( isset( $config['label'] ) && ! empty( $config['label'] ) ) { $merged_config['label'] = $config['label']; } $sanitized[] = $merged_config; } return $sanitized; } /** * Extract index config. * * @author WebDevStudios * @since 1.0.0 * * @param array $config The config. * @param string $index_id The index id. * * @return mixed|void */ private function extract_index_config( array $config, $index_id ) { foreach ( $config as $entry ) { if ( $index_id === $entry['index_id'] ) { return $entry; } } } /** * Get config. * * @author WebDevStudios * @since 1.0.0 * * @return array */ public function get_config() { $settings = $this->plugin->get_settings(); $config = $settings->get_autocomplete_config(); foreach ( $config as $key => &$entry ) { if ( ! isset( $entry['index_id'] ) ) { unset( $config[ $key ] ); continue; } $index = $this->plugin->get_index( $entry['index_id'] ); if ( null === $index ) { unset( $config[ $key ] ); continue; } $entry['index_name'] = $index->get_name(); $entry['enabled'] = true; } /** * Filters the configuration settings for Autocomplete. * * @since 1.0.0 * * @param array $config Array of configuration options to be used with Autocomplete javascript configuration. */ $config = (array) apply_filters( 'algolia_autocomplete_config', $config ); // Remove manually disabled indices. $config = array_filter( $config, function( $item ) { return (bool) $item['enabled']; } ); // Sort the indices. usort( $config, function( $a, $b ):int { return $a['position'] <=> $b['position']; } ); return $config; } } ================================================ FILE: includes/class-algolia-cli.php ================================================ * @since 1.0.0 * * @package WebDevStudios\WPSWA */ /** * Class Algolia_CLI * * Push and re-index records into Algolia indices. * * @since 1.0.0 */ class Algolia_CLI { /** * The Algolia_Plugin instance. * * @author WebDevStudios * @since 1.0.0 * * @var Algolia_Plugin */ private $plugin; /** * Algolia_CLI constructor. * * @author WebDevStudios * @since 1.0.0 */ public function __construct() { $this->plugin = Algolia_Plugin_Factory::create(); } /** * Push all records to Algolia for a given index. * * ## OPTIONS * * [] * : The id of the index without the prefix. * * [--clear] * : Clear all existing records prior to pushing the records. * * [--all] * : Re-indexes all the enabled indices. * * [--from_batch=] * : Re-index starting from the provided batch (instead of the first page). * * ## EXAMPLES * * wp algolia re-index * * @alias re-index * * @author WebDevStudios * @since 1.0.0 * * @param array $args Positional arguments. * @param array $assoc_args Associative arguments. */ public function reindex( $args, $assoc_args ) { if ( ! $this->plugin->get_api()->is_reachable() ) { WP_CLI::error( 'The configuration for this website does not allow to contact the Algolia API.' ); } $index_id = isset( $args[0] ) ? $args[0] : null; $clear = WP_CLI\Utils\get_flag_value( $assoc_args, 'clear' ); $all = WP_CLI\Utils\get_flag_value( $assoc_args, 'all' ); $from_batch = intval( WP_CLI\Utils\get_flag_value( $assoc_args, 'from_batch', 1 ) ); if ( ! $index_id && ! $all ) { WP_CLI::error( 'You need to either provide an index name or specify the --all argument to re-index all enabled indices.' ); } if ( $index_id && $all ) { WP_CLI::error( 'You can not give both an index name and the --all parameter.' ); } if ( $all ) { $indices = $this->plugin->get_indices( array( 'enabled' => true, ) ); } else { $index = $this->plugin->get_index( $index_id ); if ( ! $index ) { WP_CLI::error( sprintf( 'Index with id "%s" does not exist. Make sure you don\'t include the prefix.', $index_id ) ); } $indices = array( $index ); } foreach ( $indices as $index ) { $this->do_reindex( $index, $clear, $from_batch ); } } /** * Do reindex. * * @author WebDevStudios * @since 1.0.0 * * @param Algolia_Index $index Algolia_Index instance. * @param bool $clear Clear all existing records prior to pushing the records. * @param int $from_batch The batch to start indexing from. * * @return void */ private function do_reindex( Algolia_Index $index, $clear, $from_batch ) { if ( $clear ) { // translators: the placeholder will contain the name of the index. WP_CLI::log( sprintf( __( 'About to clear index %s...', 'wp-search-with-algolia' ), $index->get_name() ) ); $index->clear(); // translators: the placeholder will contain the name of the index. WP_CLI::success( sprintf( __( 'Correctly cleared index "%s".', 'wp-search-with-algolia' ), $index->get_name() ) ); } $total_pages = $index->get_re_index_max_num_pages() - ( $from_batch - 1 ); if ( 0 > $total_pages ) { WP_CLI::error( 'from_batch value for re-indexing is out of bounds.' ); return; } if ( 0 === $total_pages ) { $index->re_index( 1 ); WP_CLI::success( sprintf( 'Index %s was created but no entries were sent.', $index->get_name() ) ); return; } $progress = WP_CLI\Utils\make_progress_bar( sprintf( 'Processing %s batches of results.', $total_pages ), $total_pages ); $page = $from_batch; do { WP_CLI::log( sprintf( 'Indexing batch %s.', $page ) ); $index->re_index( $page++ ); WP_CLI::log( sprintf( 'Indexed batch %s.', ( $page - 1 ) ) ); $progress->tick(); } while ( $page <= ( $total_pages + $from_batch - 1 ) ); $progress->finish(); WP_CLI::success( sprintf( 'Indexed "%s" batches of results inside index "%s"', $total_pages, $index->get_name() ) ); } } ================================================ FILE: includes/class-algolia-compatibility.php ================================================ * @since 1.0.0 * * @package WebDevStudios\WPSWA */ use Yoast\WP\SEO\Memoizers\Meta_Tags_Context_Memoizer; use Yoast\WP\SEO\Surfaces\Helpers_Surface; /** * Class Algolia_Compatibility * * @since 1.0.0 */ class Algolia_Compatibility { /** * The "current language" from WPML, if available, else null. * * @author WebDevStudios * @since 1.0.0 * * @var string|null */ private $current_language; /** * Algolia_Compatibility constructor. * * @author WebDevStudios * @since 1.0.0 */ public function __construct() { add_action( 'algolia_before_get_records', array( $this, 'register_vc_shortcodes' ) ); add_action( 'algolia_before_get_records', array( $this, 'enable_yoast_frontend' ) ); add_action( 'algolia_before_get_records', array( $this, 'wpml_switch_language' ) ); add_action( 'algolia_after_get_records', array( $this, 'wpml_switch_back_language' ) ); add_action( 'algolia_excluded_post_types', [ $this, 'woocommerce_post_types' ] ); add_action( 'algolia_excluded_taxonomies', [ $this, 'woocommerce_internal_taxonomies' ] ); add_filter( 'algolia_is_block_theme', [ $this, 'maybe_block_theme' ] ); } /** * Enable Yoast frontend. * * @author WebDevStudios * @since 1.0.0 */ public function enable_yoast_frontend() { if ( ! function_exists( 'YoastSEO' ) ) { return; } if ( class_exists( 'Meta_Tags_Context_Memoizer' ) && class_exists( 'WPSEO_Replace_Vars' ) && class_exists( 'Helpers_Surface' ) ) { YoastSEO()->classes->get( Meta_Tags_Context_Memoizer::class ); YoastSEO()->classes->get( WPSEO_Replace_Vars::class ); YoastSEO()->classes->get( Helpers_Surface::class ); } } /** * Register VC shortcodes. * * @author WebDevStudios * @since 1.0.0 */ public function register_vc_shortcodes() { if ( class_exists( 'WPBMap' ) && method_exists( 'WPBMap', 'addAllMappedShortcodes' ) ) { WPBMap::addAllMappedShortcodes(); } } /** * WPML switch language. * * @author WebDevStudios * @since 1.0.0 * * @global \SitePress $sitepress The WPML global SitePress instance. * * @param mixed $post Maybe post object. * * @return void */ public function wpml_switch_language( $post ) { if ( ! $post instanceof WP_Post || ! $this->is_wpml_enabled() ) { return; } global $sitepress; $lang_info = wpml_get_language_information( null, $post->ID ); $this->current_language = $sitepress->get_current_language(); $sitepress->switch_lang( $lang_info['language_code'] ); } /** * WPML switch back language. * * @author WebDevStudios * @since 1.0.0 * * @global \SitePress $sitepress The WPML global SitePress instance. * * @param mixed $post Maybe post object. * * @return void */ public function wpml_switch_back_language( $post ) { if ( ! $post instanceof WP_Post || ! $this->is_wpml_enabled() ) { return; } global $sitepress; $sitepress->switch_lang( $this->current_language ); } /** * Check if WPML is enabled. * * @link https://github.com/algolia/algoliasearch-wordpress/issues/567 * * @author WebDevStudios * @since 1.0.0 * * @return bool */ private function is_wpml_enabled() { return function_exists( 'icl_object_id' ) && ! class_exists( 'Polylang' ); } /** * Add internal WooCommerce post types to our excluded list on Autocomplete page. * * @since 2.10.0 * * @param array $post_types Array of post types to exclude from listing. * @return array */ public function woocommerce_post_types( array $post_types ) { if ( ! defined( 'WC_PLUGIN_FILE' ) ) { return $post_types; } $post_types[] = 'patterns_ai_data'; $post_types[] = 'shop_order'; $post_types[] = 'shop_order_placehold'; $post_types[] = 'shop_order_refund'; return $post_types; } /** * Add internal WooCommerce taxonomies to our excluded list on Autocomplete page. * * @since 2.10.0 * * @param array $taxonomies Array of taxonomies to exclude from listing. * @return array */ public function woocommerce_internal_taxonomies( array $taxonomies ) { if ( ! defined( 'WC_PLUGIN_FILE' ) ) { return $taxonomies; } $taxonomies[] = 'product_visibility'; $taxonomies[] = 'product_shipping_class'; return $taxonomies; } /** * Return whether or not the current theme is block based. * * @since 2.10.3 * * @param bool $maybe_block_theme Whether or not a block theme is active. * @return bool */ public function maybe_block_theme( $maybe_block_theme ) { // return early if it has already been determined. if ( true === $maybe_block_theme ) { return $maybe_block_theme; } return wp_is_block_theme(); } } ================================================ FILE: includes/class-algolia-plugin.php ================================================ * @since 1.0.0 * * @package WebDevStudios\WPSWA */ /** * Class Algolia_Plugin * * @since 1.0.0 */ class Algolia_Plugin { const NAME = 'algolia'; /** * Instance of Algolia_API. * * @author WebDevStudios * @since 1.0.0 * * @var Algolia_API */ protected $api; /** * Instance of Algolia_Settings. * * @author WebDevStudios * @since 1.0.0 * * @var Algolia_Settings */ private $settings; /** * Instance of Algolia_Autocomplete_Config. * * @author WebDevStudios * @since 1.0.0 * * @var Algolia_Autocomplete_Config */ private $autocomplete_config; /** * Array of indices. * * @author WebDevStudios * @since 1.0.0 * * @var array */ private $indices; /** * Array of watchers. * * @author WebDevStudios * @since 1.0.0 * * @var array */ private $changes_watchers; /** * Instance of Algolia_Styles. * * @author WebDevStudios * @since 1.5.0 * * @var Algolia_Styles */ private $styles; /** * Instance of Algolia_Scripts. * * @author WebDevStudios * @since 1.5.0 * * @var Algolia_Scripts */ private $scripts; /** * Instance of Algolia_Update_Messages. * * @author WebDevStudios * @since 1.8.0 * * @var Algolia_Update_Messages */ private $update_messages; /** * Instance of Algolia_Template_Loader. * * @author WebDevStudios * @since 1.0.0 * * @var Algolia_Template_Loader */ private $template_loader; /** * Instance of Algolia_Admin. * * @author WebDevStudios * @since 2.5.4 * * @var Algolia_Admin */ public $admin; /** * Instance of Algolia_Compatibility. * * @author WebDevStudios * @since 1.0.0 * * @var Algolia_Compatibility */ private $compatibility; /** * Instance of Algoolia_Health_Panel * * @since 2.10.0 * * @var Algolia_Health_Panel */ private $health; /** * Get the singleton instance of Algolia_Plugin. * * @author WebDevStudios * @since 1.0.0 * @deprecated 1.6.0 Use Algolia_Plugin_Factory::create() * @see Algolia_Plugin_Factory::create() * * @return Algolia_Plugin */ public static function get_instance() { _deprecated_function( __METHOD__, '1.6.0', 'Algolia_Plugin_Factory::create();' ); return Algolia_Plugin_Factory::create(); } /** * Algolia_Plugin constructor. * * @author WebDevStudios * @since 1.0.0 */ public function __construct() { $this->settings = new Algolia_Settings(); $this->api = new Algolia_API( $this->settings ); $this->compatibility = new Algolia_Compatibility(); $this->styles = new Algolia_Styles(); $this->scripts = new Algolia_Scripts(); $this->update_messages = new Algolia_Update_Messages(); add_action( 'init', array( $this, 'load' ), 20 ); } /** * Load. * * @author WebDevStudios * @since 1.0.0 */ public function load() { if ( $this->api->is_reachable() ) { $this->load_indices(); $this->override_wordpress_search(); $this->autocomplete_config = new Algolia_Autocomplete_Config( $this ); $this->template_loader = new Algolia_Template_Loader( $this ); } // Load admin or public part of the plugin. if ( is_admin() ) { $this->admin = new Algolia_Admin( $this ); $this->health = new Algolia_Health_Panel( $this ); } } /** * Get the plugin name. * * The name of the plugin used to uniquely identify it within the context of * WordPress and to define internationalization functionality. * * @author WebDevStudios * @since 1.0.0 * * @return string The name of the plugin. */ public function get_name() { return self::NAME; } /** * Retrieve the version number of the plugin. * * @author WebDevStudios * @since 1.0.0 * * @return string The version number of the plugin. */ public function get_version() { return ALGOLIA_VERSION; } /** * Get the Aloglia_API. * * @author WebDevStudios * @since 1.0.0 * * @return Algolia_API */ public function get_api() { return $this->api; } /** * Get the Algolia_Settings. * * @author WebDevStudios * @since 1.0.0 * * @return Algolia_Settings */ public function get_settings() { return $this->settings; } /** * Override WordPress native search. * * Replaces native WordPress search results by Algolia ranked results. * * @author WebDevStudios * @since 1.0.0 * * @return void */ private function override_wordpress_search() { // Do not override native search if the feature is not enabled. if ( ! $this->settings->should_override_search_in_backend() ) { return; } $index_id = $this->settings->get_native_search_index_id(); $index = $this->get_index( $index_id ); if ( null === $index ) { return; } new Algolia_Search( $index ); } /** * Get the Algolia_Autocomplete_Config. * * @author WebDevStudios * @since 1.0.0 * * @return Algolia_Autocomplete_Config */ public function get_autocomplete_config() { return $this->autocomplete_config; } /** * Load indices. * * @author WebDevStudios * @since 1.0.0 */ public function load_indices() { $synced_indices_ids = $this->settings->get_synced_indices_ids(); $client = $this->get_api()->get_client(); $index_name_prefix = $this->settings->get_index_name_prefix(); // Add a searchable posts index. $searchable_post_types = get_post_types( array( 'exclude_from_search' => false, ), 'names' ); /** * Filters the array of searchable post types. * * @since 1.0.0 * * @param array $searchable_post_types Array of registered post types that are not excluded from search. * @return array $value Filtered array of post types. */ $searchable_post_types = (array) apply_filters( 'algolia_searchable_post_types', $searchable_post_types ); $this->indices[] = new Algolia_Searchable_Posts_Index( $searchable_post_types ); // Add one posts index per post type. $post_types = get_post_types(); $excluded_post_types = $this->settings->get_excluded_post_types(); foreach ( $post_types as $post_type ) { // Skip excluded post types. if ( in_array( $post_type, $excluded_post_types, true ) ) { continue; } $this->indices[] = new Algolia_Posts_Index( $post_type ); } // Add one terms index per taxonomy. $taxonomies = get_taxonomies(); $excluded_taxonomies = $this->settings->get_excluded_taxonomies(); foreach ( $taxonomies as $taxonomy ) { // Skip excluded taxonomies. if ( in_array( $taxonomy, $excluded_taxonomies, true ) ) { continue; } $this->indices[] = new Algolia_Terms_Index( $taxonomy ); } // Add the users index. $this->indices[] = new Algolia_Users_Index(); /** * Filters the array of indices to load. * * @since 1.0.0 * * @param array $indices Array of indices to load. * @return array $value Filtered array of indices. */ $this->indices = (array) apply_filters( 'algolia_indices', $this->indices ); foreach ( $this->indices as $index ) { $index->set_name_prefix( $index_name_prefix ); $index->set_client( $client ); if ( in_array( $index->get_id(), $synced_indices_ids, true ) ) { $index->set_enabled( true ); if ( $index->contains_only( 'posts' ) ) { $this->changes_watchers[] = new Algolia_Post_Changes_Watcher( $index ); } elseif ( $index->contains_only( 'terms' ) ) { $this->changes_watchers[] = new Algolia_Term_Changes_Watcher( $index ); } elseif ( $index->contains_only( 'users' ) ) { $this->changes_watchers[] = new Algolia_User_Changes_Watcher( $index ); } } } /** * Filters the array of changes watchers to work with. * * @since 1.0.0 * * @param array $change_watchers Array of watchers to work with. * @param array $indices Array of indices. * @return array $value Filtered array of watchers. */ $this->changes_watchers = (array) apply_filters( 'algolia_changes_watchers', $this->changes_watchers, $this->indices ); foreach ( $this->changes_watchers as $watcher ) { $watcher->watch(); } } /** * Get indices. * * @author WebDevStudios * @since 1.0.0 * * @param array $args Array of arguments. * * @return array */ public function get_indices( array $args = array() ) { if ( empty( $args ) ) { return $this->indices; } $indices = $this->indices; if ( isset( $args['enabled'] ) && true === $args['enabled'] ) { $indices = array_filter( $indices, function( $index ) { return $index->is_enabled(); } ); } if ( isset( $args['contains'] ) ) { $contains = (string) $args['contains']; $indices = array_filter( $indices, function( $index ) use ( $contains ) { return $index->contains_only( $contains ); } ); } return $indices; } /** * Get index. * * @author WebDevStudios * @since 1.0.0 * * @param string $index_id The ID of the index to get. * * @return Algolia_Index|null */ public function get_index( $index_id ) { foreach ( $this->indices as $index ) { if ( $index_id === $index->get_id() ) { return $index; } } return null; } /** * Get the plugin path. * * @author WebDevStudios * @since 1.0.0 * * @return string */ public function get_path() { return untrailingslashit( ALGOLIA_PATH ); } /** * Get the templates path. * * Somewhat misleading method name. * Actually returns a path segment (directory name) with trailing slash. * * @author WebDevStudios * @since 1.0.0 * @deprecated 1.8.0 Use Algolia_Template_Utils::get_filtered_theme_templates_dirname() * @see Algolia_Template_Utils::get_filtered_theme_templates_dirname() * * @return string */ public function get_templates_path() { _deprecated_function( __METHOD__, '1.8.0', 'Algolia_Template_Utils::get_filtered_theme_templates_dirname()' ); return (string) Algolia_Template_Utils::get_filtered_theme_templates_dirname(); } /** * Get the Algolia_Template_Loader. * * @author WebDevStudios * @since 1.0.0 * * @return Algolia_Template_Loader */ public function get_template_loader() { return $this->template_loader; } /** * Get the Algolia_Styles. * * @author WebDevStudios * @since 1.5.0 * * @return Algolia_Styles */ public function get_styles() { return $this->styles; } /** * Get the Algolia_Scripts. * * @author WebDevStudios * @since 1.5.0 * * @return Algolia_Scripts */ public function get_scripts() { return $this->scripts; } } ================================================ FILE: includes/class-algolia-scripts.php ================================================ * @since 1.5.0 * * @package WebDevStudios\WPSWA */ /** * Class Algolia_Scripts * * @since 1.5.0 */ class Algolia_Scripts { /** * Algolia_Scripts constructor. * * @author WebDevStudios * @since 1.5.0 */ public function __construct() { add_action( 'wp_enqueue_scripts', [ $this, 'register_scripts' ] ); } /** * Register scripts. * * @author WebDevStudios * @since 1.5.0 */ public function register_scripts() { $in_footer = Algolia_Utils::get_scripts_in_footer_argument(); $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; $ais_suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '.development' : '.production'; wp_register_script( 'algolia-search', ALGOLIA_PLUGIN_URL . 'js/algoliasearch/dist/algoliasearch-lite.umd.js', [ 'underscore', 'wp-util', ], ALGOLIA_VERSION, $in_footer ); wp_register_script( 'algolia-autocomplete', ALGOLIA_PLUGIN_URL . 'js/autocomplete.js/dist/autocomplete' . $suffix . '.js', [ 'underscore', 'wp-util', 'algolia-search', ], ALGOLIA_VERSION, $in_footer ); wp_register_script( 'algolia-autocomplete-noconflict', ALGOLIA_PLUGIN_URL . 'js/autocomplete-noconflict.js', [ 'algolia-autocomplete', ], ALGOLIA_VERSION, $in_footer ); wp_register_script( 'algolia-instantsearch', ALGOLIA_PLUGIN_URL . 'js/instantsearch.js/dist/instantsearch' . $ais_suffix . $suffix . '.js', [ 'underscore', 'wp-util', 'algolia-search', ], ALGOLIA_VERSION, $in_footer ); } } ================================================ FILE: includes/class-algolia-search.php ================================================ * @since 1.0.0 * * @package WebDevStudios\WPSWA */ use WebDevStudios\WPSWA\Algolia\AlgoliaSearch\Exceptions\AlgoliaException; /** * Class Algolia_Search * * @since 1.0.0 */ class Algolia_Search { /** * Current page hits. * * @author WebDevStudios * @since 1.0.0 * * @var array */ private $current_page_hits = []; /** * Total hits. * * @author WebDevStudios * @since 1.0.0 * * @var int */ private $total_hits; /** * Instance of Algolia_Index. * * @author WebDevStudios * @since 1.0.0 * * @var Algolia_Index */ private $index; /** * Algolia_Search constructor. * * @author WebDevStudios * @since 1.0.0 * * @param Algolia_Index $index Instance of Algolia_Index. */ public function __construct( Algolia_Index $index ) { $this->index = $index; add_action( 'loop_start', [ $this, 'begin_highlighting' ] ); add_action( 'pre_get_posts', array( $this, 'pre_get_posts' ) ); add_action( 'wp_head', [ $this, 'output_highlighting_bundled_styles' ] ); } /** * Determines if we should filter the query passed as argument. * * @author WebDevStudios * @since 1.0.0 * * @param WP_Query $query The WP_Query to check. * * @return bool */ private function should_filter_query( WP_Query $query ) { $should_filter = ! $query->is_admin && $query->is_search() && $query->is_main_query(); /** * Allow developers to override the return value of `should_filter_query()`. * * @since 1.3.0 * * @param bool $should_filter Whether Algolia should filter the search query. * @param WP_Query $query The WP_Query that was tested for Algolia Search filtering. */ return (bool) apply_filters( 'algolia_should_filter_query', $should_filter, $query ); } /** * We force the WP_Query to only return records according to Algolia's ranking. * * @author WebDevStudios * @since 1.0.0 * * @param WP_Query $query The WP_Query being filtered. * * @return void */ public function pre_get_posts( WP_Query $query ) { if ( ! $this->should_filter_query( $query ) ) { return; } $current_page = 1; if ( get_query_var( 'paged' ) ) { $current_page = get_query_var( 'paged' ); } elseif ( get_query_var( 'page' ) ) { $current_page = get_query_var( 'page' ); } /** * Filters the array of parameters used in the Algolia Index search. * * @author WebDevStudios * @since 1.0.0 * @since 1.2.0 Introduced 'highlightPreTag' and 'highlightPostTag` parameters. * * @param array $params { * Search parameters for the Algolia Index search. * * @type string $attributesToRetrieve Which attributes to retrieve. * @type int $hitsPerPage Pagination parameter. The number of hits per page to retrieve. * @type int $page Pagination parameter. The page of results to retrieve. * @type string $highlightPreTag HTML string to insert before highlights in result snippets. * @type string $highlightPostTag HTML string to insert after highlights in result snippets. * } */ $params = apply_filters( 'algolia_search_params', array( 'attributesToRetrieve' => 'post_id', 'hitsPerPage' => (int) get_option( 'posts_per_page' ), 'page' => $current_page - 1, // Algolia pages are zero indexed. 'highlightPreTag' => '', 'highlightPostTag' => '', ) ); /** * Filters the order by clause for our backend search query. * * @since 1.0.0 * * @param null $value Order by parameter. Default null. * @return string $value Filtered order by parameter. */ $order_by = apply_filters( 'algolia_search_order_by', null ); /** * Filters the order clause for our backend search query. * * @since 1.0.0 * * @param string $value Order parameter. Default 'desc'. * @return string $value Filtered order parameter. */ $order = apply_filters( 'algolia_search_order', 'desc' ); try { $results = $this->index->search( $query->query['s'], $params, $order_by, $order ); } catch ( AlgoliaException $exception ) { error_log( $exception->getMessage() ); // phpcs:ignore -- Legacy. return; } add_filter( 'found_posts', array( $this, 'found_posts' ), 10, 2 ); add_filter( 'posts_search', array( $this, 'posts_search' ), 10, 2 ); // Store the current page hits, so that we can use them for highlighting later on. foreach ( $results['hits'] as $hit ) { $this->current_page_hits[ $hit['post_id'] ] = $hit; } // Store the total number of its, so that we can hook into the `found_posts`. // This is useful for pagination. $this->total_hits = $results['nbHits']; $post_ids = array(); foreach ( $results['hits'] as $result ) { $post_ids[] = $result['post_id']; } // Make sure there are not results by tricking WordPress in trying to find // a non existing post ID. // Otherwise, the query returns all the results. if ( empty( $post_ids ) ) { $post_ids = array( 0 ); } $query->set( 'posts_per_page', $params['hitsPerPage'] ); $query->set( 'offset', 0 ); $post_types = 'any'; $maybe_post_type = filter_input( INPUT_GET, 'post_type', FILTER_SANITIZE_SPECIAL_CHARS ); if ( ! empty( $maybe_post_type ) ) { $post_type = get_post_type_object( $maybe_post_type ); if ( null !== $post_type ) { $post_types = $post_type->name; } } $query->set( 'post_type', $post_types ); $query->set( 'post__in', $post_ids ); $query->set( 'orderby', 'post__in' ); // @todo: This actually still excludes trash and auto-drafts. $query->set( 'post_status', 'any' ); } /** * This hook returns the actual real number of results available in Algolia. * * @author WebDevStudios * @since 1.0.0 * * @param int $found_posts The number of posts found. * @param WP_Query $query The WP_Query instance (passed by reference). * * @return int */ public function found_posts( $found_posts, WP_Query $query ) { return $this->should_filter_query( $query ) ? $this->total_hits : $found_posts; } /** * Filter the search SQL that is used in the WHERE clause of WP_Query. * Removes the where Like part of the queries as we consider Algolia as being the source of truth. * We don't want to filter by anything but the actual list of post_ids resulting * from the Algolia search. * * @author WebDevStudios * @since 1.0.0 * * @param string $search Search SQL for WHERE clause. * @param WP_Query $query The current WP_Query object. * * @return string */ public function posts_search( $search, WP_Query $query ) { return $this->should_filter_query( $query ) ? '' : $search; } /** * Output the bundled styles for highlighting search result matches, if enabled. * * @author WebDevStudios * @since 1.2.0 * * @return void */ public function output_highlighting_bundled_styles() { if ( ! $this->highlighting_enabled() ) { return; } /** * Filters whether or not to output styling for search highlight. * * @since 1.2.0 * * @param bool $value Whether or not to output styling CSS * @return bool $value Filtered determination. */ if ( ! apply_filters( 'algolia_search_highlighting_enable_bundled_styles', true ) ) { return; } ?> * @since 1.0.0 * * @param WP_Query $query The WP_Query. * * @return void */ public function begin_highlighting( $query ) { if ( ! $this->should_filter_query( $query ) ) { return; } if ( ! $this->highlighting_enabled() ) { return; } add_filter( 'the_title', [ $this, 'highlight_the_title' ], 10, 2 ); add_filter( 'get_the_excerpt', [ $this, 'highlight_get_the_excerpt' ], 10, 2 ); add_action( 'loop_end', [ $this, 'end_highlighting' ] ); } /** * Stop highlighting search result matches. * * This method is called on the loop_end action, where we want to stop highlighting search result matches. * * @author WebDevStudios * @since 1.0.0 * * @param WP_Query $query The WP_Query. */ public function end_highlighting( $query ) { remove_filter( 'the_title', [ $this, 'highlight_the_title' ], 10 ); remove_filter( 'get_the_excerpt', [ $this, 'highlight_get_the_excerpt' ], 10 ); remove_action( 'loop_end', [ $this, 'end_highlighting' ] ); } /** * Filter the_title, replacing it with the highlighted title from the Algolia index. * * @author WebDevStudios * @since 1.0.0 * * @param string $title The title string. * @param int $post_id The post ID. * * @return string */ public function highlight_the_title( $title, $post_id ) { $highlighted_title = $this->current_page_hits[ $post_id ]['_highlightResult']['post_title']['value'] ?? null; if ( ! empty( $highlighted_title ) ) { $title = $highlighted_title; } return $title; } /** * Filter get_the_excerpt, replacing it with the highlighted excerpt from the Algolia index. * * @author WebDevStudios * @since 1.0.0 * * @param string $excerpt The excerpt string. * @param WP_Post $post The post object. * * @return string */ public function highlight_get_the_excerpt( $excerpt, $post ) { $highlighted_excerpt = $this->current_page_hits[ $post->ID ]['_snippetResult']['content']['value'] ?? null; if ( ! empty( $highlighted_excerpt ) ) { $excerpt = $highlighted_excerpt; } return $excerpt; } /** * Determine whether highlighting is enabled. * * @author WebDevStudios * @since 1.0.0 * * @return bool */ private function highlighting_enabled() : bool { /** * Filters whether or not highlighting is enabled. * * @since 1.2.0 * * @param bool $value Whether or not highlighting is enabled. * @return mixed $value Determined highlight enabled status. */ return apply_filters( 'algolia_search_highlighting_enabled', true ); } } ================================================ FILE: includes/class-algolia-settings.php ================================================ * @since 1.0.0 * * @package WebDevStudios\WPSWA */ /** * Class Algolia_Settings * * @since 1.0.0 */ class Algolia_Settings { /** * Algolia_Settings constructor. * * @author WebDevStudios * @since 1.0.0 */ public function __construct() { add_option( 'algolia_application_id', '' ); add_option( 'algolia_search_api_key', '' ); add_option( 'algolia_api_key', '' ); add_option( 'algolia_synced_indices_ids', array() ); add_option( 'algolia_autocomplete_enabled', 'no' ); add_option( 'algolia_autocomplete_debounce', 0 ); add_option( 'algolia_autocomplete_config', array() ); add_option( 'algolia_override_native_search', 'native' ); add_option( 'algolia_instantsearch_template_version', 'legacy' ); add_option( 'algolia_index_name_prefix', 'wp_' ); add_option( 'algolia_api_is_reachable', 'no' ); add_option( 'algolia_powered_by_enabled', 'yes' ); add_option( 'algolia_insights_enabled', 'no' ); } /** * Get the Algolia Application ID. * * @author WebDevStudios * @since 1.0.0 * * @return string */ public function get_application_id() { if ( ! $this->is_application_id_in_config() ) { return (string) get_option( 'algolia_application_id', '' ); } $this->assert_constant_is_non_empty_string( ALGOLIA_APPLICATION_ID, 'ALGOLIA_APPLICATION_ID' ); return ALGOLIA_APPLICATION_ID; } /** * Get the Algolia Search-Only API Key. * * @author WebDevStudios * @since 1.0.0 * * @return string */ public function get_search_api_key() { if ( ! $this->is_search_api_key_in_config() ) { return (string) get_option( 'algolia_search_api_key', '' ); } $this->assert_constant_is_non_empty_string( ALGOLIA_SEARCH_API_KEY, 'ALGOLIA_SEARCH_API_KEY' ); return ALGOLIA_SEARCH_API_KEY; } /** * Get the Algolia Admin API Key * * @author WebDevStudios * @since 1.0.0 * * @return string */ public function get_api_key() { if ( ! $this->is_api_key_in_config() ) { return (string) get_option( 'algolia_api_key', '' ); } $this->assert_constant_is_non_empty_string( ALGOLIA_API_KEY, 'ALGOLIA_API_KEY' ); return ALGOLIA_API_KEY; } /** * Get the excluded post types. * * @author WebDevStudios * @since 1.0.0 * @deprecated 1.7.0 Use Algolia_Settings::get_excluded_post_types() * @see Algolia_Settings::get_excluded_post_types() * * @return array */ public function get_post_types_blacklist() { _deprecated_function( __METHOD__, '1.7.0', 'Algolia_Settings::get_excluded_post_types();' ); return $this->get_excluded_post_types(); } /** * Get the excluded post types. * * @author WebDevStudios * @since 1.7.0 * * @return array */ public function get_excluded_post_types() { // Default array of excluded post types. $excluded = [ 'nav_menu_item' ]; /** * Filters excluded post types. * * @since 1.0.0 * @deprecated 1.7.0 Use {@see 'algolia_excluded_post_types'} instead. * * @param array $excluded The excluded post types. */ $excluded = (array) apply_filters_deprecated( 'algolia_post_types_blacklist', [ $excluded ], '1.7.0', 'algolia_excluded_post_types' ); /** * Filters excluded post types. * * @since 1.7.0 * * @param array $excluded The excluded post types. */ $excluded = (array) apply_filters( 'algolia_excluded_post_types', $excluded ); // Native WordPress. $builtin = get_post_types( [ '_builtin' => true ] ); // Preserve posts, pages, and attachments. unset( $builtin['post'] ); unset( $builtin['page'] ); unset( $builtin['attachment'] ); foreach ( $builtin as $type ) { $excluded[] = $type; } // Native to WordPress VIP platform. $excluded[] = 'kr_request_token'; $excluded[] = 'kr_access_token'; $excluded[] = 'deprecated_log'; $excluded[] = 'async-scan-result'; $excluded[] = 'scanresult'; return array_unique( $excluded ); } /** * Get synced indices IDs. * * @author WebDevStudios * @since 1.0.0 * * @return array */ public function get_synced_indices_ids() { $ids = array(); // Gather indices used in autocomplete experience. $config = $this->get_autocomplete_config(); foreach ( $config as $index ) { if ( isset( $index['index_id'] ) ) { $ids[] = $index['index_id']; } } // Push index used in instantsearch experience. // Todo: we should allow users to index without using the shipped search UI or backend implementation. if ( $this->should_override_search_in_backend() || $this->should_override_search_with_instantsearch() ) { $ids[] = $this->get_native_search_index_id(); } /** * Filters the indices IDs that will be synced. * * @since 1.0.0 * * @param array $ids Array of IDs to sync. * @return array $value Filtered array of IDs. */ return (array) apply_filters( 'algolia_get_synced_indices_ids', $ids ); } /** * Get excluded taxonomies. * * @author WebDevStudios * @since 1.0.0 * @deprecated 1.7.0 Use Algolia_Settings::get_excluded_taxonomies() * @see Algolia_Settings::get_excluded_taxonomies() * * @return array */ public function get_taxonomies_blacklist() { _deprecated_function( __METHOD__, '1.7.0', 'Algolia_Settings::get_excluded_taxonomies();' ); return $this->get_excluded_taxonomies(); } /** * Get excluded taxonomies. * * @author WebDevStudios * @since 1.7.0 * * @return array */ public function get_excluded_taxonomies() { // Default array of excluded taxonomies. $excluded = [ 'nav_menu', 'link_category', 'post_format', 'wp_theme', 'wp_template_part_area', ]; /** * Filters excluded taxonomies. * * @since 1.0.0 * @deprecated 1.7.0 Use {@see 'algolia_excluded_taxonomies'} instead. * * @param array $excluded The excluded taxonomies. */ $excluded = (array) apply_filters_deprecated( 'algolia_taxonomies_blacklist', [ $excluded ], '1.7.0', 'algolia_excluded_taxonomies' ); /** * Filters the array of taxonomies to be excluded from syncing. * * @since 1.7.0 * * @param array $excluded Array of taxonomies to exclude. * @return array $value Filtered array of taxonomies. */ $excluded = (array) apply_filters( 'algolia_excluded_taxonomies', $excluded ); return $excluded; } /** * Get the autocomplete enabled option setting. * * @author WebDevStudios * @since 1.0.0 * * @return string Can be 'yes' or 'no'. */ public function get_autocomplete_enabled() { $enabled = get_option( 'algolia_autocomplete_enabled', 'no' ); /** * Filters the autocomplete enabled option for algolia search. * * @since 2.3.0 * * @param string $enabled Can be 'yes' or 'no'. */ return apply_filters( 'algolia_should_override_autocomplete', $enabled ); } /** * Get the autocomplete debounce timeout settings value in milliseconds. * 0 will disable the feature (default). * * @author WebDevStudios * @since 2.10.0 * * @return int Debounce value in milliseconds. */ public function get_autocomplete_debounce() { $debounce = (int) get_option( 'algolia_autocomplete_debounce', 0 ); /** * Filters the autocomplete debounce option for algolia autocomplete. * * @since 2.10.0 * * @param int Debounce value in milliseconds. */ return (int) apply_filters( 'algolia_autocomplete_debounce', $debounce ); } /** * Get the autocomplete config. * * @author WebDevStudios * @since 1.0.0 * * @return array */ public function get_autocomplete_config() { return (array) get_option( 'algolia_autocomplete_config', array() ); } /** * Get the override native search option setting. * * @author WebDevStudios * @since 1.0.0 * * @return string Can be 'native' 'backend' or 'instantsearch'. */ public function get_override_native_search() { $search_type = get_option( 'algolia_override_native_search', 'native' ); // BC compatibility. if ( 'yes' === $search_type ) { $search_type = 'backend'; } return $search_type; } /** * Determines whether we should override search in backend. * * @author WebDevStudios * @since 1.0.0 * * @return bool */ public function should_override_search_in_backend() { return $this->get_override_native_search() === 'backend' || $this->should_override_search_with_instantsearch(); } /** * Determines whether we should override search with instantsearch.js. * * @author WebDevStudios * @since 1.0.0 * * @return bool */ public function should_override_search_with_instantsearch() { $value = $this->get_override_native_search() === 'instantsearch'; /** * Filters whether or not we should override search with instantsearch.js * * @since 1.0.0 * * @param bool $value Whether or not we should override. * @return bool $value Filtered determination of override. */ return (bool) apply_filters( 'algolia_should_override_search_with_instantsearch', $value ); } /** * Get native search index ID. * * @author WebDevStudios * @since 1.0.0 * * @return string */ public function get_native_search_index_id() { /** * Filters the native search index ID. * * @since 1.0.0 * * @param string $value The index ID for native search. * @return string $vaule The filtered index ID. */ return (string) apply_filters( 'algolia_native_search_index_id', 'searchable_posts' ); } /** * Get the index name prefix. * * @author WebDevStudios * @since 1.0.0 * * @return string */ public function get_index_name_prefix() { if ( ! $this->is_index_name_prefix_in_config() ) { return (string) get_option( 'algolia_index_name_prefix', 'wp_' ); } $this->assert_constant_is_non_empty_string( ALGOLIA_INDEX_NAME_PREFIX, 'ALGOLIA_INDEX_NAME_PREFIX' ); return ALGOLIA_INDEX_NAME_PREFIX; } /** * Makes sure that constants are non empty strings. * * This makes sure that we fail early if the environment configuration is wrong. * * @author WebDevStudios * @since 1.0.0 * * @param mixed $value The constant value to check. * @param string $constant_name The constant name to check. * * @throws RuntimeException If the constant is not a string or is empty. */ protected function assert_constant_is_non_empty_string( $value, $constant_name ) { if ( ! is_string( $value ) ) { throw new RuntimeException( sprintf( 'Constant %s in wp-config.php should be a string, %s given.', $constant_name, gettype( $value ) ) ); } if ( 0 === mb_strlen( $value ) ) { throw new RuntimeException( sprintf( 'Constant %s in wp-config.php cannot be empty.', $constant_name ) ); } } /** * Determines if the ALGOLIA_APPLICATION_ID is defined. * * @author WebDevStudios * @since 1.0.0 * * @return bool */ public function is_application_id_in_config() { return defined( 'ALGOLIA_APPLICATION_ID' ); } /** * Determines if the ALGOLIA_SEARCH_API_KEY is defined. * * @author WebDevStudios * @since 1.0.0 * * @return bool */ public function is_search_api_key_in_config() { return defined( 'ALGOLIA_SEARCH_API_KEY' ); } /** * Determines if the ALGOLIA_API_KEY is defined. * * @author WebDevStudios * @since 1.0.0 * * @return bool */ public function is_api_key_in_config() { return defined( 'ALGOLIA_API_KEY' ); } /** * Determines if the ALGOLIA_INDEX_NAME_PREFIX is defined. * * @author WebDevStudios * @since 1.0.0 * @return bool */ public function is_index_name_prefix_in_config() { return defined( 'ALGOLIA_INDEX_NAME_PREFIX' ); } /** * Get the API is reachable option setting. * * @author WebDevStudios * @since 1.0.0 * * @return bool */ public function get_api_is_reachable() { $enabled = get_option( 'algolia_api_is_reachable', 'no' ); return 'yes' === $enabled; } /** * Set the API is reachable option setting. * * @author WebDevStudios * @since 1.0.0 * * @param bool $flag If the API is reachable or not, 'yes' or 'no'. */ public function set_api_is_reachable( $flag ) { $value = (bool) true === $flag ? 'yes' : 'no'; update_option( 'algolia_api_is_reachable', $value ); } /** * Determine if powered by is enabled. * * @author WebDevStudios * @since 1.0.0 * * @return bool */ public function is_powered_by_enabled() { $enabled = get_option( 'algolia_powered_by_enabled', 'yes' ); return 'yes' === $enabled; } /** * Enable the powered by option setting. * * @author WebDevStudios * @since 1.0.0 */ public function enable_powered_by() { update_option( 'algolia_powered_by_enabled', 'yes' ); } /** * Disable the powered by option setting. * * @author WebDevStudios * @since 1.0.0 */ public function disable_powered_by() { update_option( 'algolia_powered_by_enabled', 'no' ); } /** * Determine if Insights is enabled. * * @since 2.10.2 * @return bool */ public function is_insights_enabled() { return 'yes' === get_option( 'algolia_insights_enabled', 'no' ); } /** * Return the version keyword for Instantsearch version to use. * * @since 2.9.0 * * @return mixed|null */ public function get_instantsearch_template_version() { $chosen = get_option( 'algolia_instantsearch_template_version', 'legacy' ); /** * Filters the chosen InstantSearch template version. Non-numerical. * * @since 2.9.0 * * @param string $chosen Current template version. * @return string $value Final template version. */ return apply_filters( 'algolia_instantsearch_template_version', $chosen ); } /** * Return whether or not the keyword version is set to 'modern' or 'legacy'. * * @since 2.9.0 * * @return bool */ public function should_use_instantsearch_modern() { $version = $this->get_instantsearch_template_version(); return 'modern' === $version; } } ================================================ FILE: includes/class-algolia-styles.php ================================================ * @since 1.5.0 * * @package WebDevStudios\WPSWA */ /** * Class Algolia_Styles * * @since 1.5.0 */ class Algolia_Styles { /** * Algolia_Styles constructor. * * @author WebDevStudios * @since 1.5.0 */ public function __construct() { add_action( 'wp_enqueue_scripts', [ $this, 'register_styles' ] ); } /** * Register styles. * * @author WebDevStudios * @since 1.5.0 */ public function register_styles() { wp_register_style( 'algolia-autocomplete', ALGOLIA_PLUGIN_URL . 'css/algolia-autocomplete.css', [], ALGOLIA_VERSION ); wp_register_style( 'algolia-instantsearch', ALGOLIA_PLUGIN_URL . 'css/algolia-instantsearch.css', [], ALGOLIA_VERSION ); } } ================================================ FILE: includes/class-algolia-template-loader.php ================================================ * @since 1.0.0 * * @package WebDevStudios\WPSWA */ /** * Class Algolia_Template_Loader * * @since 1.0.0 */ class Algolia_Template_Loader { /** * The Algolia Plugin. * * @since 1.0.0 * * @var Algolia_Plugin */ private $plugin; /** * Algolia_Template_Loader constructor. * * @author WebDevStudios * @since 1.0.0 * * @param Algolia_Plugin $plugin The Algolia Plugin. */ public function __construct( Algolia_Plugin $plugin ) { $this->plugin = $plugin; $settings = $this->plugin->get_settings(); $is_fse = apply_filters( 'algolia_is_block_theme', false ); if ( ! $this->should_load_autocomplete() && ! $settings->should_override_search_with_instantsearch() && /** * Filters whether or not the current theme is a block based theme. * * WP Search with Algolia will help automatically detect and use this filter. * * @since 2.10.3 * * @param bool $value Whether or not the current theme is block based. Default false. */ ! apply_filters( 'algolia_is_block_theme', false ) ) { return; } $in_footer = Algolia_Utils::get_scripts_in_footer_argument(); // Inject Algolia configuration in a JavaScript variable. if ( true === $in_footer ) { add_filter( 'wp_footer', [ $this, 'load_algolia_config' ] ); } else { add_filter( 'wp_head', [ $this, 'load_algolia_config' ] ); } // Listen for native templates to override. add_filter( 'template_include', array( $this, 'template_loader' ) ); // Load autocomplete.js search experience if its enabled. if ( $this->should_load_autocomplete() ) { add_filter( 'wp_enqueue_scripts', array( $this, 'enqueue_autocomplete_scripts' ) ); if ( true === $in_footer ) { add_filter( 'wp_footer', array( $this, 'load_autocomplete_template' ) ); } else { add_filter( 'wp_head', array( $this, 'load_autocomplete_template' ) ); } } } /** * Load config. * * @author WebDevStudios * @since 1.0.0 */ public function load_algolia_config() { $settings = $this->plugin->get_settings(); $autocomplete_config = $this->plugin->get_autocomplete_config(); $config = [ 'debug' => defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG, 'application_id' => $settings->get_application_id(), 'search_api_key' => $settings->get_search_api_key(), 'powered_by_enabled' => $settings->is_powered_by_enabled(), 'insights_enabled' => $settings->is_insights_enabled(), 'search_hits_per_page' => get_option( 'posts_per_page' ), 'query' => get_search_query(), 'indices' => [], 'autocomplete' => [ 'sources' => $autocomplete_config->get_config(), /** * Filters the CSS-style selector used to locate search inputs to add Autocomplete to. * * @since 1.0.0 * * @param string $value Selector to target with. * @return string $value Updated selector. */ 'input_selector' => (string) apply_filters( 'algolia_autocomplete_input_selector', "input[name='s']:not(.no-autocomplete):not(#adminbar-search)" ), ], ]; // Inject all the indices into the config to ease instantsearch.js integrations. $indices = $this->plugin->get_indices( array( 'enabled' => true, ) ); foreach ( $indices as $index ) { $config['indices'][ $index->get_id() ] = $index->to_array(); } /** * Filters the final result of the algolia config object to be used. * * Gives developers one last change to alter the configuration. * * @since 1.0.0 * * @param array $config Array of configuration options * @return array $config Final configuration. */ $config = (array) apply_filters( 'algolia_config', $config ); echo ''; } /** * Determines whether we should load autocomplete. * * @author WebDevStudios * @since 1.0.0 * * @return bool */ private function should_load_autocomplete() { $settings = $this->plugin->get_settings(); $autocomplete = $this->plugin->get_autocomplete_config(); if ( null === $autocomplete ) { // The user has not provided his credentials yet. return false; } $config = $autocomplete->get_config(); if ( 'yes' !== $settings->get_autocomplete_enabled() ) { return false; } return ! empty( $config ); } /** * Enqueue Algolia autocomplete.js scripts. * * @author WebDevStudios * @since 1.0.0 */ public function enqueue_autocomplete_scripts() { // Enqueue the autocomplete.js default styles. wp_enqueue_style( 'algolia-autocomplete' ); // Javascript. wp_enqueue_script( 'algolia-search' ); // Enqueue the autocomplete.js library. wp_enqueue_script( 'algolia-autocomplete' ); wp_enqueue_script( 'algolia-autocomplete-noconflict' ); /** * Fires after Algolia Autocomplete assets have been enqueued. * * Allows users to easily enqueue custom styles and scripts that could depend on Autocomplete. * * @since 1.0.0 */ do_action( 'algolia_autocomplete_scripts' ); } /** * Load a template. * * Handles template usage so that we can use our own templates instead of the themes. * * Plugin templates are located in the 'templates' directory. * Customized templates are in the theme's 'algolia' directory. * * @author WebDevStudios * @since 1.0.0 * * @param mixed $template The template to load. * * @return string */ public function template_loader( $template ) { $settings = $this->plugin->get_settings(); if ( is_search() && $settings->should_override_search_with_instantsearch() ) { $is_fse = apply_filters( 'algolia_is_block_theme', false ); // Don't need a custom instantsearch template file, but still need assets. if ( $is_fse ) { $this->load_instantsearch_assets(); return $template; } return $this->load_instantsearch_template(); } return $template; } /** * Load the InstantSearch assets. * * @since 2.10.4 */ public function load_instantsearch_assets() { add_action( 'wp_enqueue_scripts', function () { // Enqueue the instantsearch.js default styles. wp_enqueue_style( 'algolia-instantsearch' ); // Enqueue the instantsearch.js library. wp_enqueue_script( 'algolia-instantsearch' ); /** * Fires after Algolia Instantsearch assets have been enqueued. * * Allow users to easily enqueue custom styles and scripts that could depend on InstantSearch. * * @since 1.0.0 */ do_action( 'algolia_instantsearch_scripts' ); } ); } /** * Load the InstantSearch template. * * @author WebDevStudios * @since 1.0.0 * * @return string */ public function load_instantsearch_template() { // Need both assets and Instantsearch template. $this->load_instantsearch_assets(); $instantsearch_is_modern = $this->plugin->get_settings()->should_use_instantsearch_modern(); $chosen_file = ( $instantsearch_is_modern ) ? 'instantsearch-modern.php' : 'instantsearch.php'; return Algolia_Template_Utils::locate_template( $chosen_file ); } /** * Load the autocomplete template. * * @author WebDevStudios * @since 1.0.0 */ public function load_autocomplete_template() { require Algolia_Template_Utils::locate_template( 'autocomplete.php' ); } /** * Locate a template. * * @author WebDevStudios * @since 1.0.0 * @deprecated 1.8.0 Use Algolia_Template_Utils::locate_template() * @see Algolia_Template_Utils::locate_template() * * @param string $file The template file. * * @return string */ private function locate_template( $file ) { _deprecated_function( __METHOD__, '1.8.0', 'Algolia_Template_Utils::locate_template()' ); return Algolia_Template_Utils::locate_template( $file ); } } ================================================ FILE: includes/class-algolia-utils.php ================================================ * @since 1.0.0 * * @package WebDevStudios\WPSWA */ /** * Class Algolia_Utils * * @since 1.0.0 */ class Algolia_Utils { /** * Retrieve term parents with separator. * * @author WebDevStudios * @since 1.0.0 * * @param int $id Term ID. * @param string $taxonomy The taxonomy. * @param string $separator Optional, default is '/'. How to separate terms. * @param bool $nicename Optional, default is false. Whether to use nice name for display. * @param array $visited Optional. Already linked to terms to prevent duplicates. * * @return string|WP_Error A list of terms parents on success, WP_Error on failure. */ public static function get_term_parents( $id, $taxonomy, $separator = '/', $nicename = false, $visited = array() ) { $chain = ''; $parent = get_term( $id, $taxonomy ); if ( is_wp_error( $parent ) ) { return $parent; } if ( $nicename ) { $name = $parent->slug; } else { $name = $parent->name; } if ( $parent->parent && ( $parent->parent !== $parent->term_id ) && ! in_array( $parent->parent, $visited, true ) ) { $visited[] = $parent->parent; $chain .= self::get_term_parents( $parent->parent, $taxonomy, $separator, $nicename, $visited ); } $chain .= $name . $separator; return $chain; } /** * Get taxonomy tree. * * This is useful when building hierarchical menus. * * Returns an array like: * array( * 'lvl0' => ['Sales', 'Marketing'], * 'lvl1' => ['Sales > Strategies', 'Marketing > Tips & Tricks'] * ... * );. * * @link https://community.algolia.com/instantsearch.js/documentation/#hierarchicalmenu * * @author WebDevStudios * @since 1.0.0 * * @param array $terms The terms. * @param string $taxonomy The taxonomy. * @param string $separator The separator. * * @return array */ public static function get_taxonomy_tree( array $terms, $taxonomy, $separator = ' > ' ) { $term_ids = wp_list_pluck( $terms, 'term_id' ); $parents = array(); foreach ( $term_ids as $term_id ) { $path = self::get_term_parents( $term_id, $taxonomy, $separator ); $parents[] = rtrim( $path, $separator ); } $terms = array(); foreach ( $parents as $parent ) { $levels = explode( $separator, $parent ); $previous_lvl = ''; foreach ( $levels as $index => $level ) { $terms[ 'lvl' . $index ][] = $previous_lvl . $level; $previous_lvl .= $level . $separator; // Make sure we have not duplicate. // The call to `array_values` ensures that we do not end up with an object in JSON. $terms[ 'lvl' . $index ] = array_values( array_unique( $terms[ 'lvl' . $index ] ) ); } } return $terms; } /** * Get post images. * * @author WebDevStudios * @since 1.0.0 * * @param int $post_id The post ID. * * @return array */ public static function get_post_images( $post_id ) { $images = array(); if ( get_post_type( $post_id ) === 'attachment' ) { $post_thumbnail_id = (int) $post_id; } else { $post_thumbnail_id = get_post_thumbnail_id( (int) $post_id ); } if ( $post_thumbnail_id ) { /** * Filters the sizes to fetch image paths for. * * @since 1.0.0 * * @param array $value Array of image sizes to fetch. Default: thumbnail. * @return array $value Amended array of images sizes. */ $sizes = (array) apply_filters( 'algolia_post_images_sizes', array( 'thumbnail' ) ); foreach ( $sizes as $size ) { $info = wp_get_attachment_image_src( $post_thumbnail_id, $size ); if ( ! $info ) { continue; } $images[ $size ] = array( 'url' => $info[0], 'width' => $info[1], 'height' => $info[2], ); } } /** * Filters the final array of images and image data. * * Parameter will be an array keyed by image size, and has * URL, width and height details for each image. * * @since 1.0.0 * @since 2.8.0 Added `$post_id` as second parameter * * @param array $images Array of images data. * @param int $post_id Current post ID being indexed. * @return array $images Final array of images data. */ return (array) apply_filters( 'algolia_get_post_images', $images, $post_id ); } /** * Prepare content. * * @author WebDevStudios * @since 1.0.0 * * @param string $content The content to prepare. * * @return string */ public static function prepare_content( $content ) { $content = self::remove_content_noise( $content ); return wp_strip_all_tags( $content ); } /** * Remove noise from content. * * @author WebDevStudios * @since 1.0.0 * * @param string $content The content to remove noise from. * * @return string */ public static function remove_content_noise( $content ) { $noise_patterns = array( // strip out comments. "''is", // strip out cdata. "''is", // Per sourceforge http://sourceforge.net/tracker/?func=detail&aid=2949097&group_id=218559&atid=1044037 // Script tags removal now preceeds style tag removal. // strip out ``` ### cdnjs ```html ``` ### npm ```sh npm install --save autocomplete.js ``` ### Bower ```sh bower install algolia-autocomplete.js -S ``` ### Source dist/ You can find the built version in [dist/](https://github.com/algolia/autocomplete.js/tree/master/dist). ### Browserify You can require it and use [Browserify](http://browserify.org/): ```js var autocomplete = require('autocomplete.js'); ``` ## Usage ### Standalone 1. Include `autocomplete.min.js` 1. Initialize the auto-completion menu calling the `autocomplete` function **Warning**: `autocomplete.js` is not compatible with the latest version algoliasearch v4 out of the box, but you can create a compatibility source by yourself like this: ```html ``` ### jQuery 1. Include `autocomplete.jquery.min.js` after including `jQuery` 1. Initialize the auto-completion menu calling the `autocomplete` jQuery plugin **Warning**: `autocomplete.js` is not compatible with the latest version algoliasearch v4, therefore we highly recommend you use algoliasearch v3 as specified in the code snippet below. ```html ``` ### Angular.JS 1. Include `autocomplete.angular.min.js` after including `jQuery` & `Angular.js` 1. Inject the `algolia.autocomplete` module 1. Add the `autocomplete`, `aa-datasets` and the optional `aa-options` attribute to your search bar **Warning**: `autocomplete.js` is not compatible with the latest version algoliasearch v4, therefore we highly recommend you use algoliasearch v3 as specified in the code snippet below. ```html
``` **Note:** You need to rely on `jQuery`, the lite version embedded in Angular.js won't work. ## Look and Feel Below is a faux mustache template describing the DOM structure of an autocomplete dropdown menu. Keep in mind that `header`, `footer`, `suggestion`, and `empty` come from the provided templates detailed [here](#datasets). ```html {{#datasets}}
{{{header}}} {{#suggestions}}
{{{suggestion}}}
{{/suggestions}} {{^suggestions}} {{{empty}}} {{/suggestions}}
{{{footer}}}
{{/datasets}} {{empty}}
``` When an end-user mouses or keys over a `.aa-suggestion`, the class `aa-cursor` will be added to it. You can use this class as a hook for styling the "under cursor" state of suggestions. Add the following CSS rules to add a default style: ```css .algolia-autocomplete { width: 100%; } .algolia-autocomplete .aa-input, .algolia-autocomplete .aa-hint { width: 100%; } .algolia-autocomplete .aa-hint { color: #999; } .algolia-autocomplete .aa-dropdown-menu { width: 100%; background-color: #fff; border: 1px solid #999; border-top: none; } .algolia-autocomplete .aa-dropdown-menu .aa-suggestion { cursor: pointer; padding: 5px 4px; } .algolia-autocomplete .aa-dropdown-menu .aa-suggestion.aa-cursor { background-color: #B2D7FF; } .algolia-autocomplete .aa-dropdown-menu .aa-suggestion em { font-weight: bold; font-style: normal; } ``` Here is what the [basic example](https://github.com/algolia/autocomplete.js/tree/master/examples) looks like: ![Basic example](./examples/basic.gif) ## Global Options When initializing an autocomplete, there are a number of global options you can configure. * `autoselect` – If `true`, the first rendered suggestion in the dropdown will automatically have the `cursor` class, and pressing `` will select it. * `autoselectOnBlur` – If `true`, when the input is blurred, the first rendered suggestion in the dropdown will automatically have the `cursor` class, and pressing `` will select it. This option should be used on mobile, see [#113](https://github.com/algolia/autocomplete.js/issues/113) * `tabAutocomplete` – If `true`, pressing tab will select the first rendered suggestion in the dropdown. Defaults to `true`. * `hint` – If `false`, the autocomplete will not show a hint. Defaults to `true`. * `debug` – If `true`, the autocomplete will not close on `blur`. Defaults to `false`. * `clearOnSelected` – If `true`, the autocomplete will empty the search box when a suggestion is selected. This is useful if you want to use this as a way to input tags using the `selected` event. * `openOnFocus` – If `true`, the dropdown menu will open when the input is focused. Defaults to `false`. * `appendTo` – If set with a DOM selector, doesn't wrap the input and appends the wrapper and dropdown menu to the first DOM element matching the selector. It automatically positions the wrapper under the input, and sets it to the same width as the input. Can't be used with `hint: true`, because `hint` requires the wrapper around the input. * `dropdownMenuContainer` – If set with a DOM selector, it overrides the container of the dropdown menu. * `templates` – An optional hash overriding the default templates. * `dropdownMenu` – the dropdown menu template. The template should include all *dataset* placeholders. * `header` – the header to prepend to the dropdown menu * `footer` – the footer to append to the dropdown menu * `empty` – the template to display when none of the datasets are returning results. The templating function is called with a context containing the underlying `query`. * `cssClasses` – An optional hash overriding the default css classes. * `root` – the root classes. Defaults to `algolia-autocomplete`. * `prefix` – the CSS class prefix of all nested elements. Defaults to `aa`. * `noPrefix` - set this to true if you wish to not use any prefix. Without this option, all nested elements classes will have a leading dash. Defaults to `false`. * `dropdownMenu` – the dropdown menu CSS class. Defaults to `dropdown-menu`. * `input` – the input CSS class. Defaults to `input`. * `hint` – the hint CSS class. Defaults to `hint`. * `suggestions` – the suggestions list CSS class. Defaults to `suggestions`. * `suggestion` – the suggestion wrapper CSS class. Defaults to `suggestion`. * `cursor` – the cursor CSS class. Defaults to `cursor`. * `dataset` – the dataset CSS class. Defaults to `dataset`. * `empty` – the empty CSS class. Defaults to `empty`. * `keyboardShortcuts` - Array of shortcut that will focus the input. For example if you want to bind `s` and `/` you can specify: `keyboardShortcuts: ['s', '/']` * `ariaLabel` - An optional string that will populate the `aria-label` attribute. ```html ``` * `minLength` – The minimum character length needed before suggestions start getting rendered. Defaults to `1`. * `autoWidth` – This option allow you to control the width of autocomplete wrapper. When `false` the autocomplete wrapper will not have the width style attribute and you are be able to put your specific width property in your css to control the wrapper. Default value is `true`. One scenario for use this option. e.g. You have a `max-width` css attribute in your `autocomplete-dropdown-menu` and you need to width grows until fill the `max-width`. In this scenario you put a `width: auto` in your autocomplete wrapper css class and the `max-width` in your autocomplete dropdown class and all done. ## Datasets An autocomplete is composed of one or more datasets. When an end-user modifies the value of the underlying input, each dataset will attempt to render suggestions for the new value. Datasets can be configured using the following options. * `source` – The backing data source for suggestions. Expected to be a function with the signature `(query, cb)`. It is expected that the function will compute the suggestion set (i.e. an array of JavaScript objects) for `query` and then invoke `cb` with said set. `cb` can be invoked synchronously or asynchronously. * `name` – The name of the dataset. This will be appended to `tt-dataset-` to form the class name of the containing DOM element. Must only consist of underscores, dashes, letters (`a-z`), and numbers. Defaults to a random number. * `displayKey` – For a given suggestion object, determines the string representation of it. This will be used when setting the value of the input control after a suggestion is selected. Can be either a key string or a function that transforms a suggestion object into a string. Defaults to `value`. Example function usage: `displayKey: function(suggestion) { return suggestion.nickname || suggestion.firstName }` * `templates` – A hash of templates to be used when rendering the dataset. Note a precompiled template is a function that takes a JavaScript object as its first argument and returns a HTML string. * `empty` – Rendered when `0` suggestions are available for the given query. Can be either a HTML string or a precompiled template. The templating function is called with a context containing `query`, `isEmpty`, and any optional arguments that may have been forwarded by the source: `function emptyTemplate({ query, isEmpty }, [forwarded args])`. * `footer` – Rendered at the bottom of the dataset. Can be either a HTML string or a precompiled template. The templating function is called with a context containing `query`, `isEmpty`, and any optional arguments that may have been forwarded by the source: `function footerTemplate({ query, isEmpty }, [forwarded args])`. * `header` – Rendered at the top of the dataset. Can be either a HTML string or a precompiled template. The templating function is called with a context containing `query`, `isEmpty`, and any optional arguments that may have been forwarded by the source: `function headerTemplate({ query, isEmpty }, [forwarded args])`. * `suggestion` – Used to render a single suggestion. The templating function is called with the `suggestion`, and any optional arguments that may have been forwarded by the source: `function suggestionTemplate(suggestion, [forwarded args])`. Defaults to the value of `displayKey` wrapped in a `p` tag i.e. `

{{value}}

`. * `debounce` – If set, will postpone the source execution until after `debounce` milliseconds have elapsed since the last time it was invoked. * `cache` - If set to `false`, subsequent identical queries will always execute the source function for suggestions. Defaults to `true`. ## Sources A few helpers are provided by default to ease the creation of Algolia-based sources. ### Hits To build a source based on Algolia's `hits` array, just use: ```js { source: autocomplete.sources.hits(indexObj, { hitsPerPage: 2 }), templates: { suggestion: function(suggestion, answer) { // FIXME } } } ``` ### PopularIn (aka "xxxxx in yyyyy") To build an Amazon-like autocomplete menu, suggesting popular queries and for the most popular one displaying the associated categories, you can use the `popularIn` source: ```js { source: autocomplete.sources.popularIn(popularQueriesIndexObj, { hitsPerPage: 3 }, { source: 'sourceAttribute', // attribute of the `popularQueries` index use to query the `index` index index: productsIndexObj, // targeted index facets: 'facetedCategoryAttribute', // facet used to enrich the most popular query maxValuesPerFacet: 3 // maximum number of facets returned }, { includeAll: true, // should it include an extra "All department" suggestion allTitle: 'All departments' // the included category label }), templates: { suggestion: function(suggestion, answer) { var value = suggestion.sourceAttribute; if (suggestion.facet) { // this is the first suggestion // and it has been enriched with the matching facet value += ' in ' + suggestion.facet.value + ' (' + suggestion.facet.count + ')'; } return value; } } } ``` ### Custom source The `source` options can also take a function. It enables you to have more control of the results returned by Algolia search. The function `function(query, callback)` takes 2 parameters * `query: String`: the text typed in the autocomplete * `callback: Function`: the callback to call at the end of your processing with the array of suggestions ```js source: function(query, callback) { var index = client.initIndex('myindex'); index.search(query, { hitsPerPage: 1, facetFilters: 'category:mycat' }).then(function(answer) { callback(answer.hits); }, function() { callback([]); }); } ``` Or by reusing an existing source: ```js var hitsSource = autocomplete.sources.hits(index, { hitsPerPage: 5 }); source: function(query, callback) { hitsSource(query, function(suggestions) { // FIXME: Do stuff with the array of returned suggestions callback(suggestions); }); } ``` ## Security ### User-generated data: protecting against XSS Malicious users may attempt to engineer XSS attacks by storing HTML/JS in their data. It is important that user-generated data be properly escaped before using it in an *autocomplete.js* template. In order to easily do that, *autocomplete.js* provides you with a helper function escaping all HTML code but the highlighting tags: ```js templates: { suggestion: function(suggestion) { var val = suggestion._highlightResult.name.value; return autocomplete.escapeHighlightedString(val); } } ``` If you did specify custom highlighting pre/post tags, you can specify them as 2nd and 3rd parameter: ```js templates: { suggestion: function(suggestion) { var val = suggestion._highlightResult.name.value; return autocomplete.escapeHighlightedString(val, '', ''); } } ``` ## FAQ ### How can I `Control`-click on results and have them open in a new tab? You'll need to update your suggestion templates to make them as `` links and not simple divs. `Control`-clicking on them will trigger the default browser behavior and open suggestions in a new tab. To also support keyboard navigation, you'll need to listen to the `autocomplete:selected` event and change `window.location` to the destination URL. Note that you might need to check the value of `context.selectionMethod` in `autocomplete:selected` first. If it's equal to `click`, you should `return` early, otherwise your main window will **also** follow the link. Here is an example of how it would look like: ```javascript autocomplete(…).on('autocomplete:selected', function(event, suggestion, dataset, context) { // Do nothing on click, as the browser will already do it if (context.selectionMethod === 'click') { return; } // Change the page, for example, on other events window.location.assign(suggestion.url); }); ``` ## Events The autocomplete component triggers the following custom events. * `autocomplete:opened` – Triggered when the dropdown menu of the autocomplete is opened. * `autocomplete:shown` – Triggered when the dropdown menu of the autocomplete is shown (opened and non-empty). * `autocomplete:empty` – Triggered when all datasets are empty. * `autocomplete:closed` – Triggered when the dropdown menu of the autocomplete is closed. * `autocomplete:updated` – Triggered when a dataset is rendered. * `autocomplete:cursorchanged` – Triggered when the dropdown menu cursor is moved to a different suggestion. The event handler will be invoked with 3 arguments: the jQuery event object, the suggestion object, and the name of the dataset the suggestion belongs to. * `autocomplete:selected` – Triggered when a suggestion from the dropdown menu is selected. The event handler will be invoked with the following arguments: the jQuery event object, the suggestion object, the name of the dataset the suggestion belongs to and a `context` object. The `context` contains a `.selectionMethod` key that can be either `click`, `enterKey`, `tabKey` or `blur`, depending how the suggestion was selected. * `autocomplete:cursorremoved` – Triggered when the cursor leaves the selections or its current index is lower than 0 * `autocomplete:autocompleted` – Triggered when the query is autocompleted. Autocompleted means the query was changed to the hint. The event handler will be invoked with 3 arguments: the jQuery event object, the suggestion object, and the name of the dataset the suggestion belongs to. * `autocomplete:redrawn` – Triggered when `appendTo` is used and the wrapper is resized/repositionned. All custom events are triggered on the element initialized as the autocomplete. ## API ### jQuery Turns any `input[type="text"]` element into an auto-completion menu. `globalOptions` is an options hash that's used to configure the autocomplete to your liking. Refer to [Global Options](#global-options) for more info regarding the available configs. Subsequent arguments (`*datasets`), are individual option hashes for datasets. For more details regarding datasets, refer to [Datasets](#datasets). ``` $(selector).autocomplete(globalOptions, datasets) ``` Example: ```js $('.search-input').autocomplete({ minLength: 3 }, { name: 'my-dataset', source: mySource }); ``` #### jQuery#autocomplete('destroy') Removes the autocomplete functionality and reverts the `input` element back to its original state. ```js $('.search-input').autocomplete('destroy'); ``` #### jQuery#autocomplete('open') Opens the dropdown menu of the autocomplete. Note that being open does not mean that the menu is visible. The menu is only visible when it is open and has content. ```js $('.search-input').autocomplete('open'); ``` #### jQuery#autocomplete('close') Closes the dropdown menu of the autocomplete. ```js $('.search-input').autocomplete('close'); ``` #### jQuery#autocomplete('val') Returns the current value of the autocomplete. The value is the text the user has entered into the `input` element. ```js var myVal = $('.search-input').autocomplete('val'); ``` #### jQuery#autocomplete('val', val) Sets the value of the autocomplete. This should be used in place of `jQuery#val`. ```js $('.search-input').autocomplete('val', myVal); ``` #### jQuery.fn.autocomplete.noConflict() Returns a reference to the autocomplete plugin and reverts `jQuery.fn.autocomplete` to its previous value. Can be used to avoid naming collisions. ```js var autocomplete = jQuery.fn.autocomplete.noConflict(); jQuery.fn._autocomplete = autocomplete; ``` ### Standalone The standalone version API is similiar to jQuery's: ```js var search = autocomplete(containerSelector, globalOptions, datasets); ``` Example: ```js var search = autocomplete('#search', { hint: false }, [{ source: autocomplete.sources.hits(index, { hitsPerPage: 5 }) }]); search.autocomplete.open(); search.autocomplete.close(); search.autocomplete.getVal(); search.autocomplete.setVal('Hey Jude'); search.autocomplete.destroy(); search.autocomplete.getWrapper(); // since autocomplete.js wraps your input into another div, you can access that div ``` You can also pass a custom Typeahead instance in Autocomplete.js constructor: ```js var search = autocomplete('#search', { hint: false }, [{ ... }], new Typeahead({ ... })); ``` #### autocomplete.noConflict() Returns a reference to the autocomplete plugin and reverts `window.autocomplete` to its previous value. Can be used to avoid naming collisions. ```js var algoliaAutocomplete = autocomplete.noConflict(); ``` ## Contributing & releasing see [CONTRIBUTING.md](./CONTRIBUTING.md) ## Credits This library has originally been forked from [Twitter's typeahead.js](https://github.com/twitter/typeahead.js) library. ================================================ FILE: js/autocomplete.js/bower.json ================================================ { "name": "algolia-autocomplete.js", "main": "dist/autocomplete.js", "version": "0.38.1", "homepage": "https://github.com/algolia/autocomplete.js", "authors": [ "Algolia Team " ], "description": "Fast and fully-featured autocomplete library", "keywords": [ "autocomplete", "typeahead" ], "license": "MIT", "ignore": [ "**/.*", "node_modules", "bower_components", "test" ] } ================================================ FILE: js/autocomplete.js/dist/autocomplete.angular.js ================================================ /*! * autocomplete.js 0.38.1 * https://github.com/algolia/autocomplete.js * Copyright 2021 Algolia, Inc. and other contributors; Licensed MIT */ /******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) /******/ return installedModules[moduleId].exports; /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ exports: {}, /******/ id: moduleId, /******/ loaded: false /******/ }; /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ // Flag the module as loaded /******/ module.loaded = true; /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ // __webpack_public_path__ /******/ __webpack_require__.p = ""; /******/ // Load entry module and return exports /******/ return __webpack_require__(0); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; module.exports = __webpack_require__(1); /***/ }, /* 1 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var angular = __webpack_require__(2); // setup DOM element var DOM = __webpack_require__(3); DOM.element = angular.element; // setup utils functions var _ = __webpack_require__(4); _.isArray = angular.isArray; _.isFunction = angular.isFunction; _.isObject = angular.isObject; _.bind = angular.element.proxy; _.each = angular.forEach; _.map = angular.element.map; _.mixin = angular.extend; _.Event = angular.element.Event; var EventBus = __webpack_require__(5); var Typeahead = __webpack_require__(6); angular.module('algolia.autocomplete', []) .directive('autocomplete', ['$parse', '$injector', function($parse, $injector) { // inject the sources in the algolia namespace if available try { $injector.get('algolia').sources = Typeahead.sources; $injector.get('algolia').escapeHighlightedString = _.escapeHighlightedString; } catch (e) { // not fatal } return { restrict: 'AC', // Only apply on an attribute or class scope: { options: '&aaOptions', datasets: '&aaDatasets' }, link: function(scope, element, attrs) { if (!element.hasClass('autocomplete') && attrs.autocomplete !== '') return; attrs = attrs; // no-unused-vars scope.options = $parse(scope.options)(scope); if (!scope.options) { scope.options = {}; } scope.datasets = $parse(scope.datasets)(scope); if (scope.datasets && !angular.isArray(scope.datasets)) { scope.datasets = [scope.datasets]; } var eventBus = new EventBus({el: element}); var autocomplete = null; // reinitialization watchers scope.$watch('options', initialize); if (angular.isArray(scope.datasets)) { scope.$watchCollection('datasets', initialize); } else { scope.$watch('datasets', initialize); } // init function function initialize() { if (autocomplete) { autocomplete.destroy(); } autocomplete = new Typeahead({ input: element, dropdownMenuContainer: scope.options.dropdownMenuContainer, eventBus: eventBus, hint: scope.options.hint, minLength: scope.options.minLength, autoselect: scope.options.autoselect, autoselectOnBlur: scope.options.autoselectOnBlur, tabAutocomplete: scope.options.tabAutocomplete, openOnFocus: scope.options.openOnFocus, templates: scope.options.templates, debug: scope.options.debug, clearOnSelected: scope.options.clearOnSelected, cssClasses: scope.options.cssClasses, datasets: scope.datasets, keyboardShortcuts: scope.options.keyboardShortcuts, appendTo: scope.options.appendTo, autoWidth: scope.options.autoWidth }); } // Propagate the selected event element.bind('autocomplete:selected', function(object, suggestion, dataset) { scope.$emit('autocomplete:selected', suggestion, dataset); }); // Propagate the autocompleted event element.bind('autocomplete:autocompleted', function(object, suggestion, dataset) { scope.$emit('autocomplete:autocompleted', suggestion, dataset); }); // Propagate the opened event element.bind('autocomplete:opened', function() { scope.$emit('autocomplete:opened'); }); // Propagate the closed event element.bind('autocomplete:closed', function() { scope.$emit('autocomplete:closed'); }); // Propagate the cursorchanged event element.bind('autocomplete:cursorchanged', function(event, suggestion, dataset) { scope.$emit('autocomplete:cursorchanged', event, suggestion, dataset); }); } }; }]); /***/ }, /* 2 */ /***/ function(module, exports) { module.exports = angular; /***/ }, /* 3 */ /***/ function(module, exports) { 'use strict'; module.exports = { element: null }; /***/ }, /* 4 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var DOM = __webpack_require__(3); function escapeRegExp(str) { return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); } module.exports = { // those methods are implemented differently // depending on which build it is, using // $... or angular... or Zepto... or require(...) isArray: null, isFunction: null, isObject: null, bind: null, each: null, map: null, mixin: null, isMsie: function(agentString) { if (agentString === undefined) { agentString = navigator.userAgent; } // from https://github.com/ded/bowser/blob/master/bowser.js if ((/(msie|trident)/i).test(agentString)) { var match = agentString.match(/(msie |rv:)(\d+(.\d+)?)/i); if (match) { return match[2]; } } return false; }, // http://stackoverflow.com/a/6969486 escapeRegExChars: function(str) { return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); }, isNumber: function(obj) { return typeof obj === 'number'; }, toStr: function toStr(s) { return s === undefined || s === null ? '' : s + ''; }, cloneDeep: function cloneDeep(obj) { var clone = this.mixin({}, obj); var self = this; this.each(clone, function(value, key) { if (value) { if (self.isArray(value)) { clone[key] = [].concat(value); } else if (self.isObject(value)) { clone[key] = self.cloneDeep(value); } } }); return clone; }, error: function(msg) { throw new Error(msg); }, every: function(obj, test) { var result = true; if (!obj) { return result; } this.each(obj, function(val, key) { if (result) { result = test.call(null, val, key, obj) && result; } }); return !!result; }, any: function(obj, test) { var found = false; if (!obj) { return found; } this.each(obj, function(val, key) { if (test.call(null, val, key, obj)) { found = true; return false; } }); return found; }, getUniqueId: (function() { var counter = 0; return function() { return counter++; }; })(), templatify: function templatify(obj) { if (this.isFunction(obj)) { return obj; } var $template = DOM.element(obj); if ($template.prop('tagName') === 'SCRIPT') { return function template() { return $template.text(); }; } return function template() { return String(obj); }; }, defer: function(fn) { setTimeout(fn, 0); }, noop: function() {}, formatPrefix: function(prefix, noPrefix) { return noPrefix ? '' : prefix + '-'; }, className: function(prefix, clazz, skipDot) { return (skipDot ? '' : '.') + prefix + clazz; }, escapeHighlightedString: function(str, highlightPreTag, highlightPostTag) { highlightPreTag = highlightPreTag || ''; var pre = document.createElement('div'); pre.appendChild(document.createTextNode(highlightPreTag)); highlightPostTag = highlightPostTag || ''; var post = document.createElement('div'); post.appendChild(document.createTextNode(highlightPostTag)); var div = document.createElement('div'); div.appendChild(document.createTextNode(str)); return div.innerHTML .replace(RegExp(escapeRegExp(pre.innerHTML), 'g'), highlightPreTag) .replace(RegExp(escapeRegExp(post.innerHTML), 'g'), highlightPostTag); } }; /***/ }, /* 5 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var namespace = 'autocomplete:'; var _ = __webpack_require__(4); var DOM = __webpack_require__(3); // constructor // ----------- function EventBus(o) { if (!o || !o.el) { _.error('EventBus initialized without el'); } this.$el = DOM.element(o.el); } // instance methods // ---------------- _.mixin(EventBus.prototype, { // ### public trigger: function(type, suggestion, dataset, context) { var event = _.Event(namespace + type); this.$el.trigger(event, [suggestion, dataset, context]); return event; } }); module.exports = EventBus; /***/ }, /* 6 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var attrsKey = 'aaAttrs'; var _ = __webpack_require__(4); var DOM = __webpack_require__(3); var EventBus = __webpack_require__(5); var Input = __webpack_require__(7); var Dropdown = __webpack_require__(16); var html = __webpack_require__(18); var css = __webpack_require__(19); // constructor // ----------- // THOUGHT: what if datasets could dynamically be added/removed? function Typeahead(o) { var $menu; var $hint; o = o || {}; if (!o.input) { _.error('missing input'); } this.isActivated = false; this.debug = !!o.debug; this.autoselect = !!o.autoselect; this.autoselectOnBlur = !!o.autoselectOnBlur; this.openOnFocus = !!o.openOnFocus; this.minLength = _.isNumber(o.minLength) ? o.minLength : 1; this.autoWidth = (o.autoWidth === undefined) ? true : !!o.autoWidth; this.clearOnSelected = !!o.clearOnSelected; this.tabAutocomplete = (o.tabAutocomplete === undefined) ? true : !!o.tabAutocomplete; o.hint = !!o.hint; if (o.hint && o.appendTo) { throw new Error('[autocomplete.js] hint and appendTo options can\'t be used at the same time'); } this.css = o.css = _.mixin({}, css, o.appendTo ? css.appendTo : {}); this.cssClasses = o.cssClasses = _.mixin({}, css.defaultClasses, o.cssClasses || {}); this.cssClasses.prefix = o.cssClasses.formattedPrefix = _.formatPrefix(this.cssClasses.prefix, this.cssClasses.noPrefix); this.listboxId = o.listboxId = [this.cssClasses.root, 'listbox', _.getUniqueId()].join('-'); var domElts = buildDom(o); this.$node = domElts.wrapper; var $input = this.$input = domElts.input; $menu = domElts.menu; $hint = domElts.hint; if (o.dropdownMenuContainer) { DOM.element(o.dropdownMenuContainer) .css('position', 'relative') // ensure the container has a relative position .append($menu.css('top', '0')); // override the top: 100% } // #705: if there's scrollable overflow, ie doesn't support // blur cancellations when the scrollbar is clicked // // #351: preventDefault won't cancel blurs in ie <= 8 $input.on('blur.aa', function($e) { var active = document.activeElement; if (_.isMsie() && ($menu[0] === active || $menu[0].contains(active))) { $e.preventDefault(); // stop immediate in order to prevent Input#_onBlur from // getting exectued $e.stopImmediatePropagation(); _.defer(function() { $input.focus(); }); } }); // #351: prevents input blur due to clicks within dropdown menu $menu.on('mousedown.aa', function($e) { $e.preventDefault(); }); this.eventBus = o.eventBus || new EventBus({el: $input}); this.dropdown = new Typeahead.Dropdown({ appendTo: o.appendTo, wrapper: this.$node, menu: $menu, datasets: o.datasets, templates: o.templates, cssClasses: o.cssClasses, minLength: this.minLength }) .onSync('suggestionClicked', this._onSuggestionClicked, this) .onSync('cursorMoved', this._onCursorMoved, this) .onSync('cursorRemoved', this._onCursorRemoved, this) .onSync('opened', this._onOpened, this) .onSync('closed', this._onClosed, this) .onSync('shown', this._onShown, this) .onSync('empty', this._onEmpty, this) .onSync('redrawn', this._onRedrawn, this) .onAsync('datasetRendered', this._onDatasetRendered, this); this.input = new Typeahead.Input({input: $input, hint: $hint}) .onSync('focused', this._onFocused, this) .onSync('blurred', this._onBlurred, this) .onSync('enterKeyed', this._onEnterKeyed, this) .onSync('tabKeyed', this._onTabKeyed, this) .onSync('escKeyed', this._onEscKeyed, this) .onSync('upKeyed', this._onUpKeyed, this) .onSync('downKeyed', this._onDownKeyed, this) .onSync('leftKeyed', this._onLeftKeyed, this) .onSync('rightKeyed', this._onRightKeyed, this) .onSync('queryChanged', this._onQueryChanged, this) .onSync('whitespaceChanged', this._onWhitespaceChanged, this); this._bindKeyboardShortcuts(o); this._setLanguageDirection(); } // instance methods // ---------------- _.mixin(Typeahead.prototype, { // ### private _bindKeyboardShortcuts: function(options) { if (!options.keyboardShortcuts) { return; } var $input = this.$input; var keyboardShortcuts = []; _.each(options.keyboardShortcuts, function(key) { if (typeof key === 'string') { key = key.toUpperCase().charCodeAt(0); } keyboardShortcuts.push(key); }); DOM.element(document).keydown(function(event) { var elt = (event.target || event.srcElement); var tagName = elt.tagName; if (elt.isContentEditable || tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA') { // already in an input return; } var which = event.which || event.keyCode; if (keyboardShortcuts.indexOf(which) === -1) { // not the right shortcut return; } $input.focus(); event.stopPropagation(); event.preventDefault(); }); }, _onSuggestionClicked: function onSuggestionClicked(type, $el) { var datum; var context = {selectionMethod: 'click'}; if (datum = this.dropdown.getDatumForSuggestion($el)) { this._select(datum, context); } }, _onCursorMoved: function onCursorMoved(event, updateInput) { var datum = this.dropdown.getDatumForCursor(); var currentCursorId = this.dropdown.getCurrentCursor().attr('id'); this.input.setActiveDescendant(currentCursorId); if (datum) { if (updateInput) { this.input.setInputValue(datum.value, true); } this.eventBus.trigger('cursorchanged', datum.raw, datum.datasetName); } }, _onCursorRemoved: function onCursorRemoved() { this.input.resetInputValue(); this._updateHint(); this.eventBus.trigger('cursorremoved'); }, _onDatasetRendered: function onDatasetRendered() { this._updateHint(); this.eventBus.trigger('updated'); }, _onOpened: function onOpened() { this._updateHint(); this.input.expand(); this.eventBus.trigger('opened'); }, _onEmpty: function onEmpty() { this.eventBus.trigger('empty'); }, _onRedrawn: function onRedrawn() { this.$node.css('top', 0 + 'px'); this.$node.css('left', 0 + 'px'); var inputRect = this.$input[0].getBoundingClientRect(); if (this.autoWidth) { this.$node.css('width', inputRect.width + 'px'); } var wrapperRect = this.$node[0].getBoundingClientRect(); var top = inputRect.bottom - wrapperRect.top; this.$node.css('top', top + 'px'); var left = inputRect.left - wrapperRect.left; this.$node.css('left', left + 'px'); this.eventBus.trigger('redrawn'); }, _onShown: function onShown() { this.eventBus.trigger('shown'); if (this.autoselect) { this.dropdown.cursorTopSuggestion(); } }, _onClosed: function onClosed() { this.input.clearHint(); this.input.removeActiveDescendant(); this.input.collapse(); this.eventBus.trigger('closed'); }, _onFocused: function onFocused() { this.isActivated = true; if (this.openOnFocus) { var query = this.input.getQuery(); if (query.length >= this.minLength) { this.dropdown.update(query); } else { this.dropdown.empty(); } this.dropdown.open(); } }, _onBlurred: function onBlurred() { var cursorDatum; var topSuggestionDatum; cursorDatum = this.dropdown.getDatumForCursor(); topSuggestionDatum = this.dropdown.getDatumForTopSuggestion(); var context = {selectionMethod: 'blur'}; if (!this.debug) { if (this.autoselectOnBlur && cursorDatum) { this._select(cursorDatum, context); } else if (this.autoselectOnBlur && topSuggestionDatum) { this._select(topSuggestionDatum, context); } else { this.isActivated = false; this.dropdown.empty(); this.dropdown.close(); } } }, _onEnterKeyed: function onEnterKeyed(type, $e) { var cursorDatum; var topSuggestionDatum; cursorDatum = this.dropdown.getDatumForCursor(); topSuggestionDatum = this.dropdown.getDatumForTopSuggestion(); var context = {selectionMethod: 'enterKey'}; if (cursorDatum) { this._select(cursorDatum, context); $e.preventDefault(); } else if (this.autoselect && topSuggestionDatum) { this._select(topSuggestionDatum, context); $e.preventDefault(); } }, _onTabKeyed: function onTabKeyed(type, $e) { if (!this.tabAutocomplete) { // Closing the dropdown enables further tabbing this.dropdown.close(); return; } var datum; var context = {selectionMethod: 'tabKey'}; if (datum = this.dropdown.getDatumForCursor()) { this._select(datum, context); $e.preventDefault(); } else { this._autocomplete(true); } }, _onEscKeyed: function onEscKeyed() { this.dropdown.close(); this.input.resetInputValue(); }, _onUpKeyed: function onUpKeyed() { var query = this.input.getQuery(); if (this.dropdown.isEmpty && query.length >= this.minLength) { this.dropdown.update(query); } else { this.dropdown.moveCursorUp(); } this.dropdown.open(); }, _onDownKeyed: function onDownKeyed() { var query = this.input.getQuery(); if (this.dropdown.isEmpty && query.length >= this.minLength) { this.dropdown.update(query); } else { this.dropdown.moveCursorDown(); } this.dropdown.open(); }, _onLeftKeyed: function onLeftKeyed() { if (this.dir === 'rtl') { this._autocomplete(); } }, _onRightKeyed: function onRightKeyed() { if (this.dir === 'ltr') { this._autocomplete(); } }, _onQueryChanged: function onQueryChanged(e, query) { this.input.clearHintIfInvalid(); if (query.length >= this.minLength) { this.dropdown.update(query); } else { this.dropdown.empty(); } this.dropdown.open(); this._setLanguageDirection(); }, _onWhitespaceChanged: function onWhitespaceChanged() { this._updateHint(); this.dropdown.open(); }, _setLanguageDirection: function setLanguageDirection() { var dir = this.input.getLanguageDirection(); if (this.dir !== dir) { this.dir = dir; this.$node.css('direction', dir); this.dropdown.setLanguageDirection(dir); } }, _updateHint: function updateHint() { var datum; var val; var query; var escapedQuery; var frontMatchRegEx; var match; datum = this.dropdown.getDatumForTopSuggestion(); if (datum && this.dropdown.isVisible() && !this.input.hasOverflow()) { val = this.input.getInputValue(); query = Input.normalizeQuery(val); escapedQuery = _.escapeRegExChars(query); // match input value, then capture trailing text frontMatchRegEx = new RegExp('^(?:' + escapedQuery + ')(.+$)', 'i'); match = frontMatchRegEx.exec(datum.value); // clear hint if there's no trailing text if (match) { this.input.setHint(val + match[1]); } else { this.input.clearHint(); } } else { this.input.clearHint(); } }, _autocomplete: function autocomplete(laxCursor) { var hint; var query; var isCursorAtEnd; var datum; hint = this.input.getHint(); query = this.input.getQuery(); isCursorAtEnd = laxCursor || this.input.isCursorAtEnd(); if (hint && query !== hint && isCursorAtEnd) { datum = this.dropdown.getDatumForTopSuggestion(); if (datum) { this.input.setInputValue(datum.value); } this.eventBus.trigger('autocompleted', datum.raw, datum.datasetName); } }, _select: function select(datum, context) { if (typeof datum.value !== 'undefined') { this.input.setQuery(datum.value); } if (this.clearOnSelected) { this.setVal(''); } else { this.input.setInputValue(datum.value, true); } this._setLanguageDirection(); var event = this.eventBus.trigger('selected', datum.raw, datum.datasetName, context); if (event.isDefaultPrevented() === false) { this.dropdown.close(); // #118: allow click event to bubble up to the body before removing // the suggestions otherwise we break event delegation _.defer(_.bind(this.dropdown.empty, this.dropdown)); } }, // ### public open: function open() { // if the menu is not activated yet, we need to update // the underlying dropdown menu to trigger the search // otherwise we're not gonna see anything if (!this.isActivated) { var query = this.input.getInputValue(); if (query.length >= this.minLength) { this.dropdown.update(query); } else { this.dropdown.empty(); } } this.dropdown.open(); }, close: function close() { this.dropdown.close(); }, setVal: function setVal(val) { // expect val to be a string, so be safe, and coerce val = _.toStr(val); if (this.isActivated) { this.input.setInputValue(val); } else { this.input.setQuery(val); this.input.setInputValue(val, true); } this._setLanguageDirection(); }, getVal: function getVal() { return this.input.getQuery(); }, destroy: function destroy() { this.input.destroy(); this.dropdown.destroy(); destroyDomStructure(this.$node, this.cssClasses); this.$node = null; }, getWrapper: function getWrapper() { return this.dropdown.$container[0]; } }); function buildDom(options) { var $input; var $wrapper; var $dropdown; var $hint; $input = DOM.element(options.input); $wrapper = DOM .element(html.wrapper.replace('%ROOT%', options.cssClasses.root)) .css(options.css.wrapper); // override the display property with the table-cell value // if the parent element is a table and the original input was a block // -> https://github.com/algolia/autocomplete.js/issues/16 if (!options.appendTo && $input.css('display') === 'block' && $input.parent().css('display') === 'table') { $wrapper.css('display', 'table-cell'); } var dropdownHtml = html.dropdown. replace('%PREFIX%', options.cssClasses.prefix). replace('%DROPDOWN_MENU%', options.cssClasses.dropdownMenu); $dropdown = DOM.element(dropdownHtml) .css(options.css.dropdown) .attr({ role: 'listbox', id: options.listboxId }); if (options.templates && options.templates.dropdownMenu) { $dropdown.html(_.templatify(options.templates.dropdownMenu)()); } $hint = $input.clone().css(options.css.hint).css(getBackgroundStyles($input)); $hint .val('') .addClass(_.className(options.cssClasses.prefix, options.cssClasses.hint, true)) .removeAttr('id name placeholder required') .prop('readonly', true) .attr({ 'aria-hidden': 'true', autocomplete: 'off', spellcheck: 'false', tabindex: -1 }); if ($hint.removeData) { $hint.removeData(); } // store the original values of the attrs that get modified // so modifications can be reverted on destroy $input.data(attrsKey, { 'aria-autocomplete': $input.attr('aria-autocomplete'), 'aria-expanded': $input.attr('aria-expanded'), 'aria-owns': $input.attr('aria-owns'), autocomplete: $input.attr('autocomplete'), dir: $input.attr('dir'), role: $input.attr('role'), spellcheck: $input.attr('spellcheck'), style: $input.attr('style'), type: $input.attr('type') }); $input .addClass(_.className(options.cssClasses.prefix, options.cssClasses.input, true)) .attr({ autocomplete: 'off', spellcheck: false, // Accessibility features // Give the field a presentation of a "select". // Combobox is the combined presentation of a single line textfield // with a listbox popup. // https://www.w3.org/WAI/PF/aria/roles#combobox role: 'combobox', // Let the screen reader know the field has an autocomplete // feature to it. 'aria-autocomplete': (options.datasets && options.datasets[0] && options.datasets[0].displayKey ? 'both' : 'list'), // Indicates whether the dropdown it controls is currently expanded or collapsed 'aria-expanded': 'false', 'aria-label': options.ariaLabel, // Explicitly point to the listbox, // which is a list of suggestions (aka options) 'aria-owns': options.listboxId }) .css(options.hint ? options.css.input : options.css.inputWithNoHint); // ie7 does not like it when dir is set to auto try { if (!$input.attr('dir')) { $input.attr('dir', 'auto'); } } catch (e) { // ignore } $wrapper = options.appendTo ? $wrapper.appendTo(DOM.element(options.appendTo).eq(0)).eq(0) : $input.wrap($wrapper).parent(); $wrapper .prepend(options.hint ? $hint : null) .append($dropdown); return { wrapper: $wrapper, input: $input, hint: $hint, menu: $dropdown }; } function getBackgroundStyles($el) { return { backgroundAttachment: $el.css('background-attachment'), backgroundClip: $el.css('background-clip'), backgroundColor: $el.css('background-color'), backgroundImage: $el.css('background-image'), backgroundOrigin: $el.css('background-origin'), backgroundPosition: $el.css('background-position'), backgroundRepeat: $el.css('background-repeat'), backgroundSize: $el.css('background-size') }; } function destroyDomStructure($node, cssClasses) { var $input = $node.find(_.className(cssClasses.prefix, cssClasses.input)); // need to remove attrs that weren't previously defined and // revert attrs that originally had a value _.each($input.data(attrsKey), function(val, key) { if (val === undefined) { $input.removeAttr(key); } else { $input.attr(key, val); } }); $input .detach() .removeClass(_.className(cssClasses.prefix, cssClasses.input, true)) .insertAfter($node); if ($input.removeData) { $input.removeData(attrsKey); } $node.remove(); } Typeahead.Dropdown = Dropdown; Typeahead.Input = Input; Typeahead.sources = __webpack_require__(20); module.exports = Typeahead; /***/ }, /* 7 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var specialKeyCodeMap; specialKeyCodeMap = { 9: 'tab', 27: 'esc', 37: 'left', 39: 'right', 13: 'enter', 38: 'up', 40: 'down' }; var _ = __webpack_require__(4); var DOM = __webpack_require__(3); var EventEmitter = __webpack_require__(8); // constructor // ----------- function Input(o) { var that = this; var onBlur; var onFocus; var onKeydown; var onInput; o = o || {}; if (!o.input) { _.error('input is missing'); } // bound functions onBlur = _.bind(this._onBlur, this); onFocus = _.bind(this._onFocus, this); onKeydown = _.bind(this._onKeydown, this); onInput = _.bind(this._onInput, this); this.$hint = DOM.element(o.hint); this.$input = DOM.element(o.input) .on('blur.aa', onBlur) .on('focus.aa', onFocus) .on('keydown.aa', onKeydown); // if no hint, noop all the hint related functions if (this.$hint.length === 0) { this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop; } // ie7 and ie8 don't support the input event // ie9 doesn't fire the input event when characters are removed // not sure if ie10 is compatible if (!_.isMsie()) { this.$input.on('input.aa', onInput); } else { this.$input.on('keydown.aa keypress.aa cut.aa paste.aa', function($e) { // if a special key triggered this, ignore it if (specialKeyCodeMap[$e.which || $e.keyCode]) { return; } // give the browser a chance to update the value of the input // before checking to see if the query changed _.defer(_.bind(that._onInput, that, $e)); }); } // the query defaults to whatever the value of the input is // on initialization, it'll most likely be an empty string this.query = this.$input.val(); // helps with calculating the width of the input's value this.$overflowHelper = buildOverflowHelper(this.$input); } // static methods // -------------- Input.normalizeQuery = function(str) { // strips leading whitespace and condenses all whitespace return (str || '').replace(/^\s*/g, '').replace(/\s{2,}/g, ' '); }; // instance methods // ---------------- _.mixin(Input.prototype, EventEmitter, { // ### private _onBlur: function onBlur() { this.resetInputValue(); this.$input.removeAttr('aria-activedescendant'); this.trigger('blurred'); }, _onFocus: function onFocus() { this.trigger('focused'); }, _onKeydown: function onKeydown($e) { // which is normalized and consistent (but not for ie) var keyName = specialKeyCodeMap[$e.which || $e.keyCode]; this._managePreventDefault(keyName, $e); if (keyName && this._shouldTrigger(keyName, $e)) { this.trigger(keyName + 'Keyed', $e); } }, _onInput: function onInput() { this._checkInputValue(); }, _managePreventDefault: function managePreventDefault(keyName, $e) { var preventDefault; var hintValue; var inputValue; switch (keyName) { case 'tab': hintValue = this.getHint(); inputValue = this.getInputValue(); preventDefault = hintValue && hintValue !== inputValue && !withModifier($e); break; case 'up': case 'down': preventDefault = !withModifier($e); break; default: preventDefault = false; } if (preventDefault) { $e.preventDefault(); } }, _shouldTrigger: function shouldTrigger(keyName, $e) { var trigger; switch (keyName) { case 'tab': trigger = !withModifier($e); break; default: trigger = true; } return trigger; }, _checkInputValue: function checkInputValue() { var inputValue; var areEquivalent; var hasDifferentWhitespace; inputValue = this.getInputValue(); areEquivalent = areQueriesEquivalent(inputValue, this.query); hasDifferentWhitespace = areEquivalent && this.query ? this.query.length !== inputValue.length : false; this.query = inputValue; if (!areEquivalent) { this.trigger('queryChanged', this.query); } else if (hasDifferentWhitespace) { this.trigger('whitespaceChanged', this.query); } }, // ### public focus: function focus() { this.$input.focus(); }, blur: function blur() { this.$input.blur(); }, getQuery: function getQuery() { return this.query; }, setQuery: function setQuery(query) { this.query = query; }, getInputValue: function getInputValue() { return this.$input.val(); }, setInputValue: function setInputValue(value, silent) { if (typeof value === 'undefined') { value = this.query; } this.$input.val(value); // silent prevents any additional events from being triggered if (silent) { this.clearHint(); } else { this._checkInputValue(); } }, expand: function expand() { this.$input.attr('aria-expanded', 'true'); }, collapse: function collapse() { this.$input.attr('aria-expanded', 'false'); }, setActiveDescendant: function setActiveDescendant(activedescendantId) { this.$input.attr('aria-activedescendant', activedescendantId); }, removeActiveDescendant: function removeActiveDescendant() { this.$input.removeAttr('aria-activedescendant'); }, resetInputValue: function resetInputValue() { this.setInputValue(this.query, true); }, getHint: function getHint() { return this.$hint.val(); }, setHint: function setHint(value) { this.$hint.val(value); }, clearHint: function clearHint() { this.setHint(''); }, clearHintIfInvalid: function clearHintIfInvalid() { var val; var hint; var valIsPrefixOfHint; var isValid; val = this.getInputValue(); hint = this.getHint(); valIsPrefixOfHint = val !== hint && hint.indexOf(val) === 0; isValid = val !== '' && valIsPrefixOfHint && !this.hasOverflow(); if (!isValid) { this.clearHint(); } }, getLanguageDirection: function getLanguageDirection() { return (this.$input.css('direction') || 'ltr').toLowerCase(); }, hasOverflow: function hasOverflow() { // 2 is arbitrary, just picking a small number to handle edge cases var constraint = this.$input.width() - 2; this.$overflowHelper.text(this.getInputValue()); return this.$overflowHelper.width() >= constraint; }, isCursorAtEnd: function() { var valueLength; var selectionStart; var range; valueLength = this.$input.val().length; selectionStart = this.$input[0].selectionStart; if (_.isNumber(selectionStart)) { return selectionStart === valueLength; } else if (document.selection) { // NOTE: this won't work unless the input has focus, the good news // is this code should only get called when the input has focus range = document.selection.createRange(); range.moveStart('character', -valueLength); return valueLength === range.text.length; } return true; }, destroy: function destroy() { this.$hint.off('.aa'); this.$input.off('.aa'); this.$hint = this.$input = this.$overflowHelper = null; } }); // helper functions // ---------------- function buildOverflowHelper($input) { return DOM.element('') .css({ // position helper off-screen position: 'absolute', visibility: 'hidden', // avoid line breaks and whitespace collapsing whiteSpace: 'pre', // use same font css as input to calculate accurate width fontFamily: $input.css('font-family'), fontSize: $input.css('font-size'), fontStyle: $input.css('font-style'), fontVariant: $input.css('font-variant'), fontWeight: $input.css('font-weight'), wordSpacing: $input.css('word-spacing'), letterSpacing: $input.css('letter-spacing'), textIndent: $input.css('text-indent'), textRendering: $input.css('text-rendering'), textTransform: $input.css('text-transform') }) .insertAfter($input); } function areQueriesEquivalent(a, b) { return Input.normalizeQuery(a) === Input.normalizeQuery(b); } function withModifier($e) { return $e.altKey || $e.ctrlKey || $e.metaKey || $e.shiftKey; } module.exports = Input; /***/ }, /* 8 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var immediate = __webpack_require__(9); var splitter = /\s+/; module.exports = { onSync: onSync, onAsync: onAsync, off: off, trigger: trigger }; function on(method, types, cb, context) { var type; if (!cb) { return this; } types = types.split(splitter); cb = context ? bindContext(cb, context) : cb; this._callbacks = this._callbacks || {}; while (type = types.shift()) { this._callbacks[type] = this._callbacks[type] || {sync: [], async: []}; this._callbacks[type][method].push(cb); } return this; } function onAsync(types, cb, context) { return on.call(this, 'async', types, cb, context); } function onSync(types, cb, context) { return on.call(this, 'sync', types, cb, context); } function off(types) { var type; if (!this._callbacks) { return this; } types = types.split(splitter); while (type = types.shift()) { delete this._callbacks[type]; } return this; } function trigger(types) { var type; var callbacks; var args; var syncFlush; var asyncFlush; if (!this._callbacks) { return this; } types = types.split(splitter); args = [].slice.call(arguments, 1); while ((type = types.shift()) && (callbacks = this._callbacks[type])) { // eslint-disable-line syncFlush = getFlush(callbacks.sync, this, [type].concat(args)); asyncFlush = getFlush(callbacks.async, this, [type].concat(args)); if (syncFlush()) { immediate(asyncFlush); } } return this; } function getFlush(callbacks, context, args) { return flush; function flush() { var cancelled; for (var i = 0, len = callbacks.length; !cancelled && i < len; i += 1) { // only cancel if the callback explicitly returns false cancelled = callbacks[i].apply(context, args) === false; } return !cancelled; } } function bindContext(fn, context) { return fn.bind ? fn.bind(context) : function() { fn.apply(context, [].slice.call(arguments, 0)); }; } /***/ }, /* 9 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; var types = [ __webpack_require__(10), __webpack_require__(12), __webpack_require__(13), __webpack_require__(14), __webpack_require__(15) ]; var draining; var currentQueue; var queueIndex = -1; var queue = []; var scheduled = false; function cleanUpNextTick() { if (!draining || !currentQueue) { return; } draining = false; if (currentQueue.length) { queue = currentQueue.concat(queue); } else { queueIndex = -1; } if (queue.length) { nextTick(); } } //named nextTick for less confusing stack traces function nextTick() { if (draining) { return; } scheduled = false; draining = true; var len = queue.length; var timeout = setTimeout(cleanUpNextTick); while (len) { currentQueue = queue; queue = []; while (currentQueue && ++queueIndex < len) { currentQueue[queueIndex].run(); } queueIndex = -1; len = queue.length; } currentQueue = null; queueIndex = -1; draining = false; clearTimeout(timeout); } var scheduleDrain; var i = -1; var len = types.length; while (++i < len) { if (types[i] && types[i].test && types[i].test()) { scheduleDrain = types[i].install(nextTick); break; } } // v8 likes predictible objects function Item(fun, array) { this.fun = fun; this.array = array; } Item.prototype.run = function () { var fun = this.fun; var array = this.array; switch (array.length) { case 0: return fun(); case 1: return fun(array[0]); case 2: return fun(array[0], array[1]); case 3: return fun(array[0], array[1], array[2]); default: return fun.apply(null, array); } }; module.exports = immediate; function immediate(task) { var args = new Array(arguments.length - 1); if (arguments.length > 1) { for (var i = 1; i < arguments.length; i++) { args[i - 1] = arguments[i]; } } queue.push(new Item(task, args)); if (!scheduled && !draining) { scheduled = true; scheduleDrain(); } } /***/ }, /* 10 */ /***/ function(module, exports, __webpack_require__) { /* WEBPACK VAR INJECTION */(function(process) {'use strict'; exports.test = function () { // Don't get fooled by e.g. browserify environments. return (typeof process !== 'undefined') && !process.browser; }; exports.install = function (func) { return function () { process.nextTick(func); }; }; /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(11))) /***/ }, /* 11 */ /***/ function(module, exports) { // shim for using process in browser var process = module.exports = {}; // cached from whatever global is present so that test runners that stub it // don't break things. But we need to wrap it in a try catch in case it is // wrapped in strict mode code which doesn't define any globals. It's inside a // function because try/catches deoptimize in certain engines. var cachedSetTimeout; var cachedClearTimeout; function defaultSetTimout() { throw new Error('setTimeout has not been defined'); } function defaultClearTimeout () { throw new Error('clearTimeout has not been defined'); } (function () { try { if (typeof setTimeout === 'function') { cachedSetTimeout = setTimeout; } else { cachedSetTimeout = defaultSetTimout; } } catch (e) { cachedSetTimeout = defaultSetTimout; } try { if (typeof clearTimeout === 'function') { cachedClearTimeout = clearTimeout; } else { cachedClearTimeout = defaultClearTimeout; } } catch (e) { cachedClearTimeout = defaultClearTimeout; } } ()) function runTimeout(fun) { if (cachedSetTimeout === setTimeout) { //normal enviroments in sane situations return setTimeout(fun, 0); } // if setTimeout wasn't available but was latter defined if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) { cachedSetTimeout = setTimeout; return setTimeout(fun, 0); } try { // when when somebody has screwed with setTimeout but no I.E. maddness return cachedSetTimeout(fun, 0); } catch(e){ try { // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally return cachedSetTimeout.call(null, fun, 0); } catch(e){ // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error return cachedSetTimeout.call(this, fun, 0); } } } function runClearTimeout(marker) { if (cachedClearTimeout === clearTimeout) { //normal enviroments in sane situations return clearTimeout(marker); } // if clearTimeout wasn't available but was latter defined if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) { cachedClearTimeout = clearTimeout; return clearTimeout(marker); } try { // when when somebody has screwed with setTimeout but no I.E. maddness return cachedClearTimeout(marker); } catch (e){ try { // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally return cachedClearTimeout.call(null, marker); } catch (e){ // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error. // Some versions of I.E. have different rules for clearTimeout vs setTimeout return cachedClearTimeout.call(this, marker); } } } var queue = []; var draining = false; var currentQueue; var queueIndex = -1; function cleanUpNextTick() { if (!draining || !currentQueue) { return; } draining = false; if (currentQueue.length) { queue = currentQueue.concat(queue); } else { queueIndex = -1; } if (queue.length) { drainQueue(); } } function drainQueue() { if (draining) { return; } var timeout = runTimeout(cleanUpNextTick); draining = true; var len = queue.length; while(len) { currentQueue = queue; queue = []; while (++queueIndex < len) { if (currentQueue) { currentQueue[queueIndex].run(); } } queueIndex = -1; len = queue.length; } currentQueue = null; draining = false; runClearTimeout(timeout); } process.nextTick = function (fun) { var args = new Array(arguments.length - 1); if (arguments.length > 1) { for (var i = 1; i < arguments.length; i++) { args[i - 1] = arguments[i]; } } queue.push(new Item(fun, args)); if (queue.length === 1 && !draining) { runTimeout(drainQueue); } }; // v8 likes predictible objects function Item(fun, array) { this.fun = fun; this.array = array; } Item.prototype.run = function () { this.fun.apply(null, this.array); }; process.title = 'browser'; process.browser = true; process.env = {}; process.argv = []; process.version = ''; // empty string to avoid regexp issues process.versions = {}; function noop() {} process.on = noop; process.addListener = noop; process.once = noop; process.off = noop; process.removeListener = noop; process.removeAllListeners = noop; process.emit = noop; process.binding = function (name) { throw new Error('process.binding is not supported'); }; process.cwd = function () { return '/' }; process.chdir = function (dir) { throw new Error('process.chdir is not supported'); }; process.umask = function() { return 0; }; /***/ }, /* 12 */ /***/ function(module, exports) { /* WEBPACK VAR INJECTION */(function(global) {'use strict'; //based off rsvp https://github.com/tildeio/rsvp.js //license https://github.com/tildeio/rsvp.js/blob/master/LICENSE //https://github.com/tildeio/rsvp.js/blob/master/lib/rsvp/asap.js var Mutation = global.MutationObserver || global.WebKitMutationObserver; exports.test = function () { return Mutation; }; exports.install = function (handle) { var called = 0; var observer = new Mutation(handle); var element = global.document.createTextNode(''); observer.observe(element, { characterData: true }); return function () { element.data = (called = ++called % 2); }; }; /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()))) /***/ }, /* 13 */ /***/ function(module, exports) { /* WEBPACK VAR INJECTION */(function(global) {'use strict'; exports.test = function () { if (global.setImmediate) { // we can only get here in IE10 // which doesn't handel postMessage well return false; } return typeof global.MessageChannel !== 'undefined'; }; exports.install = function (func) { var channel = new global.MessageChannel(); channel.port1.onmessage = func; return function () { channel.port2.postMessage(0); }; }; /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()))) /***/ }, /* 14 */ /***/ function(module, exports) { /* WEBPACK VAR INJECTION */(function(global) {'use strict'; exports.test = function () { return 'document' in global && 'onreadystatechange' in global.document.createElement('script'); }; exports.install = function (handle) { return function () { // Create a ================================================ FILE: js/autocomplete.js/examples/basic_angular.html ================================================
================================================ FILE: js/autocomplete.js/examples/basic_jquery.html ================================================

Basic example

================================================ FILE: js/autocomplete.js/examples/index.html ================================================ Autocomplete.js example Welcome to the Autocomplete.js examples:
  1. Basic example
  2. Basic Angular.js example
  3. Basic jQuery example
  4. Test playground
  5. Test playground (Angular.js)
  6. Test playground (jQuery)
================================================ FILE: js/autocomplete.js/index.js ================================================ 'use strict'; module.exports = require('./src/standalone/'); ================================================ FILE: js/autocomplete.js/index_angular.js ================================================ 'use strict'; module.exports = require('./src/angular/directive.js'); ================================================ FILE: js/autocomplete.js/index_jquery.js ================================================ 'use strict'; module.exports = require('./src/jquery/plugin.js'); ================================================ FILE: js/autocomplete.js/karma.conf.js ================================================ 'use strict'; module.exports = function(config) { config.set({ basePath: '', frameworks: ['jasmine', 'sinon'], reporters: ['progress', 'coverage', 'coveralls'], browsers: ['PhantomJS'], coverageReporter: { type: 'lcov', dir: 'coverage/' }, webpack: { devtool: 'inline-source-map', module: { postLoaders: [{ test: /\.js$/, exclude: /(test|node_modules)\//, loader: 'istanbul-instrumenter' }] } }, preprocessors: { 'test/unit/**/*.js': ['webpack', 'sourcemap'] }, webpackMiddleware: { noInfo: true }, plugins: [ 'karma-jasmine', 'karma-sinon', 'karma-phantomjs-launcher', 'karma-chrome-launcher', 'karma-opera-launcher', 'karma-safari-launcher', 'karma-firefox-launcher', 'karma-coverage', 'karma-coveralls', 'karma-sourcemap-loader', 'karma-webpack' ], files: [ './node_modules/es6-promise/dist/es6-promise.auto.js', 'test/unit/**/*.js' ] }); }; ================================================ FILE: js/autocomplete.js/package.json ================================================ { "_from": "autocomplete.js@^0.38.0", "_id": "autocomplete.js@0.38.1", "_inBundle": false, "_integrity": "sha512-6pSJzuRMY3pqpozt+SXThl2DmJfma8Bi3SVFbZHS0PW/N72bOUv+Db0jAh2cWOhTsA4X+GNmKvIl8wExJTnN9w==", "_location": "/autocomplete.js", "_phantomChildren": {}, "_requested": { "type": "range", "registry": true, "raw": "autocomplete.js@^0.38.0", "name": "autocomplete.js", "escapedName": "autocomplete.js", "rawSpec": "^0.38.0", "saveSpec": null, "fetchSpec": "^0.38.0" }, "_requiredBy": [ "#USER" ], "_resolved": "https://registry.npmjs.org/autocomplete.js/-/autocomplete.js-0.38.1.tgz", "_shasum": "9b006c985d996165ebbc62af33f5b4c32d209cc2", "_spec": "autocomplete.js@^0.38.0", "_where": "/Users/asharirfan/code/wp-algolia/app/public/wp-content/plugins/wp-search-with-algolia-dev", "bugs": { "url": "https://github.com/algolia/autocomplete.js/issues" }, "bundleDependencies": false, "dependencies": { "immediate": "^3.2.3" }, "deprecated": false, "description": "Fast and fully-featured autocomplete library", "devDependencies": { "angular": "^1.6.4", "angular-mocks": "^1.6.4", "babel-eslint": "^7.2.3", "chai": "^3.5.0", "colors": "^1.1.2", "conventional-changelog-cli": "^1.3.1", "doctoc": "^1.3.0", "es6-promise": "^4.2.8", "eslint": "1.5.1", "eslint-config-airbnb": "0.1.0", "eslint-config-algolia": "3.0.0", "execa": "^1.0.0", "grunt": "^1.0.1", "grunt-banner": "^0.6.0", "grunt-cli": "1.2.0", "grunt-concurrent": "^2.3.1", "grunt-contrib-clean": "^1.1.0", "grunt-contrib-concat": "^1.0.1", "grunt-contrib-connect": "^1.0.2", "grunt-contrib-uglify": "^2.2.0", "grunt-contrib-watch": "^1.0.0", "grunt-eslint": "^17.2.0", "grunt-exec": "^1.0.1", "grunt-sed": "^0.1.1", "grunt-step": "^1.0.0", "grunt-umd": "^2.3.6", "grunt-webpack": "^1.0.14", "istanbul-instrumenter-loader": "^1.0.0", "jasmine-core": "^2.6.2", "jasmine-jquery": "^2.1.1", "jquery": "^3.2.1", "json": "^9.0.6", "karma": "^1.7.0", "karma-chrome-launcher": "^2.1.1", "karma-coverage": "^1.1.1", "karma-coveralls": "^1.1.2", "karma-firefox-launcher": "^1.0.1", "karma-jasmine": "^1.0.2", "karma-opera-launcher": "^1.0.0", "karma-phantomjs-launcher": "^1.0.4", "karma-safari-launcher": "^1.0.0", "karma-sinon": "^1.0.5", "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^2.0.3", "mocha": "^3.4.1", "mversion": "^1.12.0", "netlify": "^2.0.1", "node-static": "^0.7.8", "phantomjs-prebuilt": "^2.1.12", "replace-in-file": "^3.4.2", "semver": "^5.3.0", "sinon": "^1.17.6", "webpack": "^1.13.2", "webpack-dev-server": "^1.16.1", "yiewd": "^0.6.0" }, "homepage": "https://github.com/algolia/autocomplete.js", "keywords": [ "autocomplete", "typeahead" ], "license": "MIT", "main": "index.js", "name": "autocomplete.js", "repository": { "type": "git", "url": "git+https://github.com/algolia/autocomplete.js.git" }, "scripts": { "build": "grunt", "dev": "grunt dev", "docs:netlify": "./scripts/netlify-deploy.js", "lint": "grunt lint", "release": "./scripts/release.sh", "server": "grunt server", "test": "karma start --single-run && grunt lint", "test:ci": "./test/ci.sh", "test:watch": "karma start" }, "version": "0.38.1" } ================================================ FILE: js/autocomplete.js/scripts/netlify-deploy.js ================================================ #!/usr/bin/env node 'use strict'; /* eslint-disable no-console */ const execa = require('execa'); const replace = require('replace-in-file'); const NetlifyAPI = require('netlify'); const client = new NetlifyAPI(process.env.NETLIFY_API_KEY); function logStdOut(opts) { console.log(opts.stdout); } if (!process.env.NETLIFY_API_KEY || !process.env.NETLIFY_SITE_ID) { console.warn( 'Both NETLIFY_API_KEY and NETLIFY_SITE_ID are required. ' + 'They can be found on ' + 'https://app.netlify.com/sites/autocompletejs-playgrounds/settings/general' + ' and https://app.netlify.com/account/applications' ); process.exit(0); } execa('yarn', ['build']) .then(logStdOut) .then(() => execa('rm', ['-rf', 'netlify-dist'])) .then(() => execa('mkdir', ['-p', 'netlify-dist/examples'])) .then(() => execa('cp', ['-r', 'examples', 'netlify-dist'])) .then(() => execa('mv', ['netlify-dist/examples/index.html', 'netlify-dist'])) .then(() => replace({ files: 'netlify-dist/index.html', from: /href="\.\./g, to: 'href=".' }) ) .then(() => execa('mkdir', ['-p', 'netlify-dist/test'])) .then(() => execa('cp', [ 'test/playground.css', 'test/playground.html', 'test/playground_angular.html', 'test/playground_jquery.html', 'netlify-dist/test' ]) ) .then(() => execa('cp', ['-r', 'dist', 'netlify-dist'])) .then(() => replace({ files: [ 'netlify-dist/examples/basic.html', 'netlify-dist/examples/basic_angular.html', 'netlify-dist/examples/basic_jquery.html' ], from: /https:\/\/cdn.jsdelivr.net\/autocomplete.js\/0/g, to: '../dist' }) ) .then(() => client.deploy(process.env.NETLIFY_SITE_ID, 'netlify-dist', { draft: true, message: process.env.TRAVIS_COMMIT_MESSAGE || '' }) ) .then(({deploy: {id, name, deploy_ssl_url: url}}) => console.log( '🕸 site is available at ' + url + '\n\n' + 'Deploy details available at https://app.netlify.com/sites/' + name + '/deploys/' + id ) ); ================================================ FILE: js/autocomplete.js/scripts/release.sh ================================================ #!/usr/bin/env bash function error_exit { echo "release: $1" 1>&2 exit 1 } if [[ $# -eq 0 ]] ; then error_exit "use ``npm run release [major|minor|patch|x.x.x]``" fi currentVersion=$(json -f package.json version) if [[ $1 != 'patch' && $1 != 'minor' && $1 != 'major' ]] then nextVersion=$1 else nextVersion=$(semver $currentVersion -i $1) fi semver $nextVersion -r ">$currentVersion" || error_exit "Cannot bump from $currentVersion to $nextVersion" if ! npm owner ls | grep -q "$(npm whoami)" then error_exit "Not an owner of the npm repo, ask for it" fi currentBranch=$(git rev-parse --abbrev-ref HEAD) if [[ $currentBranch != 'master' ]]; then error_exit "You must be on master branch" fi if [[ -n $(git status --porcelain) ]]; then error_exit "Release: Working tree is not clean (git status)" fi echo "module.exports = \"${nextVersion}\";" > version.js yarn && mversion $nextVersion && yarn build && conventional-changelog --infile CHANGELOG.md --same-file --preset angular && doctoc --notitle --maxlevel 3 README.md && git add README.md CHANGELOG.md package.json bower.json version.js dist/ && git commit -m $nextVersion && git tag v$nextVersion && git push && git push --tags && npm publish || error_exit "Something went wrong, check log, be careful and start over" ================================================ FILE: js/autocomplete.js/src/angular/directive.js ================================================ 'use strict'; var angular = require('angular'); // setup DOM element var DOM = require('../common/dom.js'); DOM.element = angular.element; // setup utils functions var _ = require('../common/utils.js'); _.isArray = angular.isArray; _.isFunction = angular.isFunction; _.isObject = angular.isObject; _.bind = angular.element.proxy; _.each = angular.forEach; _.map = angular.element.map; _.mixin = angular.extend; _.Event = angular.element.Event; var EventBus = require('../autocomplete/event_bus.js'); var Typeahead = require('../autocomplete/typeahead.js'); angular.module('algolia.autocomplete', []) .directive('autocomplete', ['$parse', '$injector', function($parse, $injector) { // inject the sources in the algolia namespace if available try { $injector.get('algolia').sources = Typeahead.sources; $injector.get('algolia').escapeHighlightedString = _.escapeHighlightedString; } catch (e) { // not fatal } return { restrict: 'AC', // Only apply on an attribute or class scope: { options: '&aaOptions', datasets: '&aaDatasets' }, link: function(scope, element, attrs) { if (!element.hasClass('autocomplete') && attrs.autocomplete !== '') return; attrs = attrs; // no-unused-vars scope.options = $parse(scope.options)(scope); if (!scope.options) { scope.options = {}; } scope.datasets = $parse(scope.datasets)(scope); if (scope.datasets && !angular.isArray(scope.datasets)) { scope.datasets = [scope.datasets]; } var eventBus = new EventBus({el: element}); var autocomplete = null; // reinitialization watchers scope.$watch('options', initialize); if (angular.isArray(scope.datasets)) { scope.$watchCollection('datasets', initialize); } else { scope.$watch('datasets', initialize); } // init function function initialize() { if (autocomplete) { autocomplete.destroy(); } autocomplete = new Typeahead({ input: element, dropdownMenuContainer: scope.options.dropdownMenuContainer, eventBus: eventBus, hint: scope.options.hint, minLength: scope.options.minLength, autoselect: scope.options.autoselect, autoselectOnBlur: scope.options.autoselectOnBlur, tabAutocomplete: scope.options.tabAutocomplete, openOnFocus: scope.options.openOnFocus, templates: scope.options.templates, debug: scope.options.debug, clearOnSelected: scope.options.clearOnSelected, cssClasses: scope.options.cssClasses, datasets: scope.datasets, keyboardShortcuts: scope.options.keyboardShortcuts, appendTo: scope.options.appendTo, autoWidth: scope.options.autoWidth }); } // Propagate the selected event element.bind('autocomplete:selected', function(object, suggestion, dataset) { scope.$emit('autocomplete:selected', suggestion, dataset); }); // Propagate the autocompleted event element.bind('autocomplete:autocompleted', function(object, suggestion, dataset) { scope.$emit('autocomplete:autocompleted', suggestion, dataset); }); // Propagate the opened event element.bind('autocomplete:opened', function() { scope.$emit('autocomplete:opened'); }); // Propagate the closed event element.bind('autocomplete:closed', function() { scope.$emit('autocomplete:closed'); }); // Propagate the cursorchanged event element.bind('autocomplete:cursorchanged', function(event, suggestion, dataset) { scope.$emit('autocomplete:cursorchanged', event, suggestion, dataset); }); } }; }]); ================================================ FILE: js/autocomplete.js/src/autocomplete/css.js ================================================ 'use strict'; var _ = require('../common/utils.js'); var css = { wrapper: { position: 'relative', display: 'inline-block' }, hint: { position: 'absolute', top: '0', left: '0', borderColor: 'transparent', boxShadow: 'none', // #741: fix hint opacity issue on iOS opacity: '1' }, input: { position: 'relative', verticalAlign: 'top', backgroundColor: 'transparent' }, inputWithNoHint: { position: 'relative', verticalAlign: 'top' }, dropdown: { position: 'absolute', top: '100%', left: '0', zIndex: '100', display: 'none' }, suggestions: { display: 'block' }, suggestion: { whiteSpace: 'nowrap', cursor: 'pointer' }, suggestionChild: { whiteSpace: 'normal' }, ltr: { left: '0', right: 'auto' }, rtl: { left: 'auto', right: '0' }, defaultClasses: { root: 'algolia-autocomplete', prefix: 'aa', noPrefix: false, dropdownMenu: 'dropdown-menu', input: 'input', hint: 'hint', suggestions: 'suggestions', suggestion: 'suggestion', cursor: 'cursor', dataset: 'dataset', empty: 'empty' }, // will be merged with the default ones if appendTo is used appendTo: { wrapper: { position: 'absolute', zIndex: '100', display: 'none' }, input: {}, inputWithNoHint: {}, dropdown: { display: 'block' } } }; // ie specific styling if (_.isMsie()) { // ie6-8 (and 9?) doesn't fire hover and click events for elements with // transparent backgrounds, for a workaround, use 1x1 transparent gif _.mixin(css.input, { backgroundImage: 'url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)' }); } // ie7 and under specific styling if (_.isMsie() && _.isMsie() <= 7) { // if someone can tell me why this is necessary to align // the hint with the query in ie7, i'll send you $5 - @JakeHarding _.mixin(css.input, {marginTop: '-1px'}); } module.exports = css; ================================================ FILE: js/autocomplete.js/src/autocomplete/dataset.js ================================================ 'use strict'; var datasetKey = 'aaDataset'; var valueKey = 'aaValue'; var datumKey = 'aaDatum'; var _ = require('../common/utils.js'); var DOM = require('../common/dom.js'); var html = require('./html.js'); var css = require('./css.js'); var EventEmitter = require('./event_emitter.js'); // constructor // ----------- function Dataset(o) { o = o || {}; o.templates = o.templates || {}; if (!o.source) { _.error('missing source'); } if (o.name && !isValidName(o.name)) { _.error('invalid dataset name: ' + o.name); } // tracks the last query the dataset was updated for this.query = null; this._isEmpty = true; this.highlight = !!o.highlight; this.name = typeof o.name === 'undefined' || o.name === null ? _.getUniqueId() : o.name; this.source = o.source; this.displayFn = getDisplayFn(o.display || o.displayKey); this.debounce = o.debounce; this.cache = o.cache !== false; this.templates = getTemplates(o.templates, this.displayFn); this.css = _.mixin({}, css, o.appendTo ? css.appendTo : {}); this.cssClasses = o.cssClasses = _.mixin({}, css.defaultClasses, o.cssClasses || {}); this.cssClasses.prefix = o.cssClasses.formattedPrefix || _.formatPrefix(this.cssClasses.prefix, this.cssClasses.noPrefix); var clazz = _.className(this.cssClasses.prefix, this.cssClasses.dataset); this.$el = o.$menu && o.$menu.find(clazz + '-' + this.name).length > 0 ? DOM.element(o.$menu.find(clazz + '-' + this.name)[0]) : DOM.element( html.dataset.replace('%CLASS%', this.name) .replace('%PREFIX%', this.cssClasses.prefix) .replace('%DATASET%', this.cssClasses.dataset) ); this.$menu = o.$menu; this.clearCachedSuggestions(); } // static methods // -------------- Dataset.extractDatasetName = function extractDatasetName(el) { return DOM.element(el).data(datasetKey); }; Dataset.extractValue = function extractValue(el) { return DOM.element(el).data(valueKey); }; Dataset.extractDatum = function extractDatum(el) { var datum = DOM.element(el).data(datumKey); if (typeof datum === 'string') { // Zepto has an automatic deserialization of the // JSON encoded data attribute datum = JSON.parse(datum); } return datum; }; // instance methods // ---------------- _.mixin(Dataset.prototype, EventEmitter, { // ### private _render: function render(query, suggestions) { if (!this.$el) { return; } var that = this; var hasSuggestions; var renderArgs = [].slice.call(arguments, 2); this.$el.empty(); hasSuggestions = suggestions && suggestions.length; this._isEmpty = !hasSuggestions; if (!hasSuggestions && this.templates.empty) { this.$el .html(getEmptyHtml.apply(this, renderArgs)) .prepend(that.templates.header ? getHeaderHtml.apply(this, renderArgs) : null) .append(that.templates.footer ? getFooterHtml.apply(this, renderArgs) : null); } else if (hasSuggestions) { this.$el .html(getSuggestionsHtml.apply(this, renderArgs)) .prepend(that.templates.header ? getHeaderHtml.apply(this, renderArgs) : null) .append(that.templates.footer ? getFooterHtml.apply(this, renderArgs) : null); } else if (suggestions && !Array.isArray(suggestions)) { throw new TypeError('suggestions must be an array'); } if (this.$menu) { this.$menu.addClass( this.cssClasses.prefix + (hasSuggestions ? 'with' : 'without') + '-' + this.name ).removeClass( this.cssClasses.prefix + (hasSuggestions ? 'without' : 'with') + '-' + this.name ); } this.trigger('rendered', query); function getEmptyHtml() { var args = [].slice.call(arguments, 0); args = [{query: query, isEmpty: true}].concat(args); return that.templates.empty.apply(this, args); } function getSuggestionsHtml() { var args = [].slice.call(arguments, 0); var $suggestions; var nodes; var self = this; var suggestionsHtml = html.suggestions. replace('%PREFIX%', this.cssClasses.prefix). replace('%SUGGESTIONS%', this.cssClasses.suggestions); $suggestions = DOM .element(suggestionsHtml) .css(this.css.suggestions); // jQuery#append doesn't support arrays as the first argument // until version 1.8, see http://bugs.jquery.com/ticket/11231 nodes = _.map(suggestions, getSuggestionNode); $suggestions.append.apply($suggestions, nodes); return $suggestions; function getSuggestionNode(suggestion) { var $el; var suggestionHtml = html.suggestion. replace('%PREFIX%', self.cssClasses.prefix). replace('%SUGGESTION%', self.cssClasses.suggestion); $el = DOM.element(suggestionHtml) .attr({ role: 'option', id: ['option', Math.floor(Math.random() * 100000000)].join('-') }) .append(that.templates.suggestion.apply(this, [suggestion].concat(args))); $el.data(datasetKey, that.name); $el.data(valueKey, that.displayFn(suggestion) || undefined); // this led to undefined return value $el.data(datumKey, JSON.stringify(suggestion)); $el.children().each(function() { DOM.element(this).css(self.css.suggestionChild); }); return $el; } } function getHeaderHtml() { var args = [].slice.call(arguments, 0); args = [{query: query, isEmpty: !hasSuggestions}].concat(args); return that.templates.header.apply(this, args); } function getFooterHtml() { var args = [].slice.call(arguments, 0); args = [{query: query, isEmpty: !hasSuggestions}].concat(args); return that.templates.footer.apply(this, args); } }, // ### public getRoot: function getRoot() { return this.$el; }, update: function update(query) { function handleSuggestions(suggestions) { // if the update has been canceled or if the query has changed // do not render the suggestions as they've become outdated if (!this.canceled && query === this.query) { // concat all the other arguments that could have been passed // to the render function, and forward them to _render var extraArgs = [].slice.call(arguments, 1); this.cacheSuggestions(query, suggestions, extraArgs); this._render.apply(this, [query, suggestions].concat(extraArgs)); } } this.query = query; this.canceled = false; if (this.shouldFetchFromCache(query)) { handleSuggestions.apply(this, [this.cachedSuggestions].concat(this.cachedRenderExtraArgs)); } else { var that = this; var execSource = function() { // When the call is debounced the condition avoid to do a useless // request with the last character when the input has been cleared if (!that.canceled) { that.source(query, handleSuggestions.bind(that)); } }; if (this.debounce) { var later = function() { that.debounceTimeout = null; execSource(); }; clearTimeout(this.debounceTimeout); this.debounceTimeout = setTimeout(later, this.debounce); } else { execSource(); } } }, cacheSuggestions: function cacheSuggestions(query, suggestions, extraArgs) { this.cachedQuery = query; this.cachedSuggestions = suggestions; this.cachedRenderExtraArgs = extraArgs; }, shouldFetchFromCache: function shouldFetchFromCache(query) { return this.cache && this.cachedQuery === query && this.cachedSuggestions && this.cachedSuggestions.length; }, clearCachedSuggestions: function clearCachedSuggestions() { delete this.cachedQuery; delete this.cachedSuggestions; delete this.cachedRenderExtraArgs; }, cancel: function cancel() { this.canceled = true; }, clear: function clear() { if (this.$el) { this.cancel(); this.$el.empty(); this.trigger('rendered', ''); } }, isEmpty: function isEmpty() { return this._isEmpty; }, destroy: function destroy() { this.clearCachedSuggestions(); this.$el = null; } }); // helper functions // ---------------- function getDisplayFn(display) { display = display || 'value'; return _.isFunction(display) ? display : displayFn; function displayFn(obj) { return obj[display]; } } function getTemplates(templates, displayFn) { return { empty: templates.empty && _.templatify(templates.empty), header: templates.header && _.templatify(templates.header), footer: templates.footer && _.templatify(templates.footer), suggestion: templates.suggestion || suggestionTemplate }; function suggestionTemplate(context) { return '

' + displayFn(context) + '

'; } } function isValidName(str) { // dashes, underscores, letters, and numbers return (/^[_a-zA-Z0-9-]+$/).test(str); } module.exports = Dataset; ================================================ FILE: js/autocomplete.js/src/autocomplete/dropdown.js ================================================ 'use strict'; var _ = require('../common/utils.js'); var DOM = require('../common/dom.js'); var EventEmitter = require('./event_emitter.js'); var Dataset = require('./dataset.js'); var css = require('./css.js'); // constructor // ----------- function Dropdown(o) { var that = this; var onSuggestionClick; var onSuggestionMouseEnter; var onSuggestionMouseLeave; o = o || {}; if (!o.menu) { _.error('menu is required'); } if (!_.isArray(o.datasets) && !_.isObject(o.datasets)) { _.error('1 or more datasets required'); } if (!o.datasets) { _.error('datasets is required'); } this.isOpen = false; this.isEmpty = true; this.minLength = o.minLength || 0; this.templates = {}; this.appendTo = o.appendTo || false; this.css = _.mixin({}, css, o.appendTo ? css.appendTo : {}); this.cssClasses = o.cssClasses = _.mixin({}, css.defaultClasses, o.cssClasses || {}); this.cssClasses.prefix = o.cssClasses.formattedPrefix || _.formatPrefix(this.cssClasses.prefix, this.cssClasses.noPrefix); // bound functions onSuggestionClick = _.bind(this._onSuggestionClick, this); onSuggestionMouseEnter = _.bind(this._onSuggestionMouseEnter, this); onSuggestionMouseLeave = _.bind(this._onSuggestionMouseLeave, this); var cssClass = _.className(this.cssClasses.prefix, this.cssClasses.suggestion); this.$menu = DOM.element(o.menu) .on('mouseenter.aa', cssClass, onSuggestionMouseEnter) .on('mouseleave.aa', cssClass, onSuggestionMouseLeave) .on('click.aa', cssClass, onSuggestionClick); this.$container = o.appendTo ? o.wrapper : this.$menu; if (o.templates && o.templates.header) { this.templates.header = _.templatify(o.templates.header); this.$menu.prepend(this.templates.header()); } if (o.templates && o.templates.empty) { this.templates.empty = _.templatify(o.templates.empty); this.$empty = DOM.element('
' + '
'); this.$menu.append(this.$empty); this.$empty.hide(); } this.datasets = _.map(o.datasets, function(oDataset) { return initializeDataset(that.$menu, oDataset, o.cssClasses); }); _.each(this.datasets, function(dataset) { var root = dataset.getRoot(); if (root && root.parent().length === 0) { that.$menu.append(root); } dataset.onSync('rendered', that._onRendered, that); }); if (o.templates && o.templates.footer) { this.templates.footer = _.templatify(o.templates.footer); this.$menu.append(this.templates.footer()); } var self = this; DOM.element(window).resize(function() { self._redraw(); }); } // instance methods // ---------------- _.mixin(Dropdown.prototype, EventEmitter, { // ### private _onSuggestionClick: function onSuggestionClick($e) { this.trigger('suggestionClicked', DOM.element($e.currentTarget)); }, _onSuggestionMouseEnter: function onSuggestionMouseEnter($e) { var elt = DOM.element($e.currentTarget); if (elt.hasClass(_.className(this.cssClasses.prefix, this.cssClasses.cursor, true))) { // we're already on the cursor // => we're probably entering it again after leaving it for a nested div return; } this._removeCursor(); // Fixes iOS double tap behaviour, by modifying the DOM right before the // native href clicks happens, iOS will requires another tap to follow // a suggestion that has an element inside // https://www.google.com/search?q=ios+double+tap+bug+href var suggestion = this; setTimeout(function() { // this exact line, when inside the main loop, will trigger a double tap bug // on iOS devices suggestion._setCursor(elt, false); }, 0); }, _onSuggestionMouseLeave: function onSuggestionMouseLeave($e) { // $e.relatedTarget is the `EventTarget` the pointing device entered to if ($e.relatedTarget) { var elt = DOM.element($e.relatedTarget); if (elt.closest('.' + _.className(this.cssClasses.prefix, this.cssClasses.cursor, true)).length > 0) { // our father is a cursor // => it means we're just leaving the suggestion for a nested div return; } } this._removeCursor(); this.trigger('cursorRemoved'); }, _onRendered: function onRendered(e, query) { this.isEmpty = _.every(this.datasets, isDatasetEmpty); if (this.isEmpty) { if (query.length >= this.minLength) { this.trigger('empty'); } if (this.$empty) { if (query.length < this.minLength) { this._hide(); } else { var html = this.templates.empty({ query: this.datasets[0] && this.datasets[0].query }); this.$empty.html(html); this.$empty.show(); this._show(); } } else if (_.any(this.datasets, hasEmptyTemplate)) { if (query.length < this.minLength) { this._hide(); } else { this._show(); } } else { this._hide(); } } else if (this.isOpen) { if (this.$empty) { this.$empty.empty(); this.$empty.hide(); } if (query.length >= this.minLength) { this._show(); } else { this._hide(); } } this.trigger('datasetRendered'); function isDatasetEmpty(dataset) { return dataset.isEmpty(); } function hasEmptyTemplate(dataset) { return dataset.templates && dataset.templates.empty; } }, _hide: function() { this.$container.hide(); }, _show: function() { // can't use jQuery#show because $menu is a span element we want // display: block; not dislay: inline; this.$container.css('display', 'block'); this._redraw(); this.trigger('shown'); }, _redraw: function redraw() { if (!this.isOpen || !this.appendTo) return; this.trigger('redrawn'); }, _getSuggestions: function getSuggestions() { return this.$menu.find(_.className(this.cssClasses.prefix, this.cssClasses.suggestion)); }, _getCursor: function getCursor() { return this.$menu.find(_.className(this.cssClasses.prefix, this.cssClasses.cursor)).first(); }, _setCursor: function setCursor($el, updateInput) { $el.first() .addClass(_.className(this.cssClasses.prefix, this.cssClasses.cursor, true)) .attr('aria-selected', 'true'); this.trigger('cursorMoved', updateInput); }, _removeCursor: function removeCursor() { this._getCursor() .removeClass(_.className(this.cssClasses.prefix, this.cssClasses.cursor, true)) .removeAttr('aria-selected'); }, _moveCursor: function moveCursor(increment) { var $suggestions; var $oldCursor; var newCursorIndex; var $newCursor; if (!this.isOpen) { return; } $oldCursor = this._getCursor(); $suggestions = this._getSuggestions(); this._removeCursor(); // shifting before and after modulo to deal with -1 index newCursorIndex = $suggestions.index($oldCursor) + increment; newCursorIndex = (newCursorIndex + 1) % ($suggestions.length + 1) - 1; if (newCursorIndex === -1) { this.trigger('cursorRemoved'); return; } else if (newCursorIndex < -1) { newCursorIndex = $suggestions.length - 1; } this._setCursor($newCursor = $suggestions.eq(newCursorIndex), true); // in the case of scrollable overflow // make sure the cursor is visible in the menu this._ensureVisible($newCursor); }, _ensureVisible: function ensureVisible($el) { var elTop; var elBottom; var menuScrollTop; var menuHeight; elTop = $el.position().top; elBottom = elTop + $el.height() + parseInt($el.css('margin-top'), 10) + parseInt($el.css('margin-bottom'), 10); menuScrollTop = this.$menu.scrollTop(); menuHeight = this.$menu.height() + parseInt(this.$menu.css('padding-top'), 10) + parseInt(this.$menu.css('padding-bottom'), 10); if (elTop < 0) { this.$menu.scrollTop(menuScrollTop + elTop); } else if (menuHeight < elBottom) { this.$menu.scrollTop(menuScrollTop + (elBottom - menuHeight)); } }, // ### public close: function close() { if (this.isOpen) { this.isOpen = false; this._removeCursor(); this._hide(); this.trigger('closed'); } }, open: function open() { if (!this.isOpen) { this.isOpen = true; if (!this.isEmpty) { this._show(); } this.trigger('opened'); } }, setLanguageDirection: function setLanguageDirection(dir) { this.$menu.css(dir === 'ltr' ? this.css.ltr : this.css.rtl); }, moveCursorUp: function moveCursorUp() { this._moveCursor(-1); }, moveCursorDown: function moveCursorDown() { this._moveCursor(+1); }, getDatumForSuggestion: function getDatumForSuggestion($el) { var datum = null; if ($el.length) { datum = { raw: Dataset.extractDatum($el), value: Dataset.extractValue($el), datasetName: Dataset.extractDatasetName($el) }; } return datum; }, getCurrentCursor: function getCurrentCursor() { return this._getCursor().first(); }, getDatumForCursor: function getDatumForCursor() { return this.getDatumForSuggestion(this._getCursor().first()); }, getDatumForTopSuggestion: function getDatumForTopSuggestion() { return this.getDatumForSuggestion(this._getSuggestions().first()); }, cursorTopSuggestion: function cursorTopSuggestion() { this._setCursor(this._getSuggestions().first(), false); }, update: function update(query) { _.each(this.datasets, updateDataset); function updateDataset(dataset) { dataset.update(query); } }, empty: function empty() { _.each(this.datasets, clearDataset); this.isEmpty = true; function clearDataset(dataset) { dataset.clear(); } }, isVisible: function isVisible() { return this.isOpen && !this.isEmpty; }, destroy: function destroy() { this.$menu.off('.aa'); this.$menu = null; _.each(this.datasets, destroyDataset); function destroyDataset(dataset) { dataset.destroy(); } } }); // helper functions // ---------------- Dropdown.Dataset = Dataset; function initializeDataset($menu, oDataset, cssClasses) { return new Dropdown.Dataset(_.mixin({$menu: $menu, cssClasses: cssClasses}, oDataset)); } module.exports = Dropdown; ================================================ FILE: js/autocomplete.js/src/autocomplete/event_bus.js ================================================ 'use strict'; var namespace = 'autocomplete:'; var _ = require('../common/utils.js'); var DOM = require('../common/dom.js'); // constructor // ----------- function EventBus(o) { if (!o || !o.el) { _.error('EventBus initialized without el'); } this.$el = DOM.element(o.el); } // instance methods // ---------------- _.mixin(EventBus.prototype, { // ### public trigger: function(type, suggestion, dataset, context) { var event = _.Event(namespace + type); this.$el.trigger(event, [suggestion, dataset, context]); return event; } }); module.exports = EventBus; ================================================ FILE: js/autocomplete.js/src/autocomplete/event_emitter.js ================================================ 'use strict'; var immediate = require('immediate'); var splitter = /\s+/; module.exports = { onSync: onSync, onAsync: onAsync, off: off, trigger: trigger }; function on(method, types, cb, context) { var type; if (!cb) { return this; } types = types.split(splitter); cb = context ? bindContext(cb, context) : cb; this._callbacks = this._callbacks || {}; while (type = types.shift()) { this._callbacks[type] = this._callbacks[type] || {sync: [], async: []}; this._callbacks[type][method].push(cb); } return this; } function onAsync(types, cb, context) { return on.call(this, 'async', types, cb, context); } function onSync(types, cb, context) { return on.call(this, 'sync', types, cb, context); } function off(types) { var type; if (!this._callbacks) { return this; } types = types.split(splitter); while (type = types.shift()) { delete this._callbacks[type]; } return this; } function trigger(types) { var type; var callbacks; var args; var syncFlush; var asyncFlush; if (!this._callbacks) { return this; } types = types.split(splitter); args = [].slice.call(arguments, 1); while ((type = types.shift()) && (callbacks = this._callbacks[type])) { // eslint-disable-line syncFlush = getFlush(callbacks.sync, this, [type].concat(args)); asyncFlush = getFlush(callbacks.async, this, [type].concat(args)); if (syncFlush()) { immediate(asyncFlush); } } return this; } function getFlush(callbacks, context, args) { return flush; function flush() { var cancelled; for (var i = 0, len = callbacks.length; !cancelled && i < len; i += 1) { // only cancel if the callback explicitly returns false cancelled = callbacks[i].apply(context, args) === false; } return !cancelled; } } function bindContext(fn, context) { return fn.bind ? fn.bind(context) : function() { fn.apply(context, [].slice.call(arguments, 0)); }; } ================================================ FILE: js/autocomplete.js/src/autocomplete/html.js ================================================ 'use strict'; module.exports = { wrapper: '', dropdown: '', dataset: '
', suggestions: '', suggestion: '
' }; ================================================ FILE: js/autocomplete.js/src/autocomplete/input.js ================================================ 'use strict'; var specialKeyCodeMap; specialKeyCodeMap = { 9: 'tab', 27: 'esc', 37: 'left', 39: 'right', 13: 'enter', 38: 'up', 40: 'down' }; var _ = require('../common/utils.js'); var DOM = require('../common/dom.js'); var EventEmitter = require('./event_emitter.js'); // constructor // ----------- function Input(o) { var that = this; var onBlur; var onFocus; var onKeydown; var onInput; o = o || {}; if (!o.input) { _.error('input is missing'); } // bound functions onBlur = _.bind(this._onBlur, this); onFocus = _.bind(this._onFocus, this); onKeydown = _.bind(this._onKeydown, this); onInput = _.bind(this._onInput, this); this.$hint = DOM.element(o.hint); this.$input = DOM.element(o.input) .on('blur.aa', onBlur) .on('focus.aa', onFocus) .on('keydown.aa', onKeydown); // if no hint, noop all the hint related functions if (this.$hint.length === 0) { this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop; } // ie7 and ie8 don't support the input event // ie9 doesn't fire the input event when characters are removed // not sure if ie10 is compatible if (!_.isMsie()) { this.$input.on('input.aa', onInput); } else { this.$input.on('keydown.aa keypress.aa cut.aa paste.aa', function($e) { // if a special key triggered this, ignore it if (specialKeyCodeMap[$e.which || $e.keyCode]) { return; } // give the browser a chance to update the value of the input // before checking to see if the query changed _.defer(_.bind(that._onInput, that, $e)); }); } // the query defaults to whatever the value of the input is // on initialization, it'll most likely be an empty string this.query = this.$input.val(); // helps with calculating the width of the input's value this.$overflowHelper = buildOverflowHelper(this.$input); } // static methods // -------------- Input.normalizeQuery = function(str) { // strips leading whitespace and condenses all whitespace return (str || '').replace(/^\s*/g, '').replace(/\s{2,}/g, ' '); }; // instance methods // ---------------- _.mixin(Input.prototype, EventEmitter, { // ### private _onBlur: function onBlur() { this.resetInputValue(); this.$input.removeAttr('aria-activedescendant'); this.trigger('blurred'); }, _onFocus: function onFocus() { this.trigger('focused'); }, _onKeydown: function onKeydown($e) { // which is normalized and consistent (but not for ie) var keyName = specialKeyCodeMap[$e.which || $e.keyCode]; this._managePreventDefault(keyName, $e); if (keyName && this._shouldTrigger(keyName, $e)) { this.trigger(keyName + 'Keyed', $e); } }, _onInput: function onInput() { this._checkInputValue(); }, _managePreventDefault: function managePreventDefault(keyName, $e) { var preventDefault; var hintValue; var inputValue; switch (keyName) { case 'tab': hintValue = this.getHint(); inputValue = this.getInputValue(); preventDefault = hintValue && hintValue !== inputValue && !withModifier($e); break; case 'up': case 'down': preventDefault = !withModifier($e); break; default: preventDefault = false; } if (preventDefault) { $e.preventDefault(); } }, _shouldTrigger: function shouldTrigger(keyName, $e) { var trigger; switch (keyName) { case 'tab': trigger = !withModifier($e); break; default: trigger = true; } return trigger; }, _checkInputValue: function checkInputValue() { var inputValue; var areEquivalent; var hasDifferentWhitespace; inputValue = this.getInputValue(); areEquivalent = areQueriesEquivalent(inputValue, this.query); hasDifferentWhitespace = areEquivalent && this.query ? this.query.length !== inputValue.length : false; this.query = inputValue; if (!areEquivalent) { this.trigger('queryChanged', this.query); } else if (hasDifferentWhitespace) { this.trigger('whitespaceChanged', this.query); } }, // ### public focus: function focus() { this.$input.focus(); }, blur: function blur() { this.$input.blur(); }, getQuery: function getQuery() { return this.query; }, setQuery: function setQuery(query) { this.query = query; }, getInputValue: function getInputValue() { return this.$input.val(); }, setInputValue: function setInputValue(value, silent) { if (typeof value === 'undefined') { value = this.query; } this.$input.val(value); // silent prevents any additional events from being triggered if (silent) { this.clearHint(); } else { this._checkInputValue(); } }, expand: function expand() { this.$input.attr('aria-expanded', 'true'); }, collapse: function collapse() { this.$input.attr('aria-expanded', 'false'); }, setActiveDescendant: function setActiveDescendant(activedescendantId) { this.$input.attr('aria-activedescendant', activedescendantId); }, removeActiveDescendant: function removeActiveDescendant() { this.$input.removeAttr('aria-activedescendant'); }, resetInputValue: function resetInputValue() { this.setInputValue(this.query, true); }, getHint: function getHint() { return this.$hint.val(); }, setHint: function setHint(value) { this.$hint.val(value); }, clearHint: function clearHint() { this.setHint(''); }, clearHintIfInvalid: function clearHintIfInvalid() { var val; var hint; var valIsPrefixOfHint; var isValid; val = this.getInputValue(); hint = this.getHint(); valIsPrefixOfHint = val !== hint && hint.indexOf(val) === 0; isValid = val !== '' && valIsPrefixOfHint && !this.hasOverflow(); if (!isValid) { this.clearHint(); } }, getLanguageDirection: function getLanguageDirection() { return (this.$input.css('direction') || 'ltr').toLowerCase(); }, hasOverflow: function hasOverflow() { // 2 is arbitrary, just picking a small number to handle edge cases var constraint = this.$input.width() - 2; this.$overflowHelper.text(this.getInputValue()); return this.$overflowHelper.width() >= constraint; }, isCursorAtEnd: function() { var valueLength; var selectionStart; var range; valueLength = this.$input.val().length; selectionStart = this.$input[0].selectionStart; if (_.isNumber(selectionStart)) { return selectionStart === valueLength; } else if (document.selection) { // NOTE: this won't work unless the input has focus, the good news // is this code should only get called when the input has focus range = document.selection.createRange(); range.moveStart('character', -valueLength); return valueLength === range.text.length; } return true; }, destroy: function destroy() { this.$hint.off('.aa'); this.$input.off('.aa'); this.$hint = this.$input = this.$overflowHelper = null; } }); // helper functions // ---------------- function buildOverflowHelper($input) { return DOM.element('') .css({ // position helper off-screen position: 'absolute', visibility: 'hidden', // avoid line breaks and whitespace collapsing whiteSpace: 'pre', // use same font css as input to calculate accurate width fontFamily: $input.css('font-family'), fontSize: $input.css('font-size'), fontStyle: $input.css('font-style'), fontVariant: $input.css('font-variant'), fontWeight: $input.css('font-weight'), wordSpacing: $input.css('word-spacing'), letterSpacing: $input.css('letter-spacing'), textIndent: $input.css('text-indent'), textRendering: $input.css('text-rendering'), textTransform: $input.css('text-transform') }) .insertAfter($input); } function areQueriesEquivalent(a, b) { return Input.normalizeQuery(a) === Input.normalizeQuery(b); } function withModifier($e) { return $e.altKey || $e.ctrlKey || $e.metaKey || $e.shiftKey; } module.exports = Input; ================================================ FILE: js/autocomplete.js/src/autocomplete/typeahead.js ================================================ 'use strict'; var attrsKey = 'aaAttrs'; var _ = require('../common/utils.js'); var DOM = require('../common/dom.js'); var EventBus = require('./event_bus.js'); var Input = require('./input.js'); var Dropdown = require('./dropdown.js'); var html = require('./html.js'); var css = require('./css.js'); // constructor // ----------- // THOUGHT: what if datasets could dynamically be added/removed? function Typeahead(o) { var $menu; var $hint; o = o || {}; if (!o.input) { _.error('missing input'); } this.isActivated = false; this.debug = !!o.debug; this.autoselect = !!o.autoselect; this.autoselectOnBlur = !!o.autoselectOnBlur; this.openOnFocus = !!o.openOnFocus; this.minLength = _.isNumber(o.minLength) ? o.minLength : 1; this.autoWidth = (o.autoWidth === undefined) ? true : !!o.autoWidth; this.clearOnSelected = !!o.clearOnSelected; this.tabAutocomplete = (o.tabAutocomplete === undefined) ? true : !!o.tabAutocomplete; o.hint = !!o.hint; if (o.hint && o.appendTo) { throw new Error('[autocomplete.js] hint and appendTo options can\'t be used at the same time'); } this.css = o.css = _.mixin({}, css, o.appendTo ? css.appendTo : {}); this.cssClasses = o.cssClasses = _.mixin({}, css.defaultClasses, o.cssClasses || {}); this.cssClasses.prefix = o.cssClasses.formattedPrefix = _.formatPrefix(this.cssClasses.prefix, this.cssClasses.noPrefix); this.listboxId = o.listboxId = [this.cssClasses.root, 'listbox', _.getUniqueId()].join('-'); var domElts = buildDom(o); this.$node = domElts.wrapper; var $input = this.$input = domElts.input; $menu = domElts.menu; $hint = domElts.hint; if (o.dropdownMenuContainer) { DOM.element(o.dropdownMenuContainer) .css('position', 'relative') // ensure the container has a relative position .append($menu.css('top', '0')); // override the top: 100% } // #705: if there's scrollable overflow, ie doesn't support // blur cancellations when the scrollbar is clicked // // #351: preventDefault won't cancel blurs in ie <= 8 $input.on('blur.aa', function($e) { var active = document.activeElement; if (_.isMsie() && ($menu[0] === active || $menu[0].contains(active))) { $e.preventDefault(); // stop immediate in order to prevent Input#_onBlur from // getting exectued $e.stopImmediatePropagation(); _.defer(function() { $input.focus(); }); } }); // #351: prevents input blur due to clicks within dropdown menu $menu.on('mousedown.aa', function($e) { $e.preventDefault(); }); this.eventBus = o.eventBus || new EventBus({el: $input}); this.dropdown = new Typeahead.Dropdown({ appendTo: o.appendTo, wrapper: this.$node, menu: $menu, datasets: o.datasets, templates: o.templates, cssClasses: o.cssClasses, minLength: this.minLength }) .onSync('suggestionClicked', this._onSuggestionClicked, this) .onSync('cursorMoved', this._onCursorMoved, this) .onSync('cursorRemoved', this._onCursorRemoved, this) .onSync('opened', this._onOpened, this) .onSync('closed', this._onClosed, this) .onSync('shown', this._onShown, this) .onSync('empty', this._onEmpty, this) .onSync('redrawn', this._onRedrawn, this) .onAsync('datasetRendered', this._onDatasetRendered, this); this.input = new Typeahead.Input({input: $input, hint: $hint}) .onSync('focused', this._onFocused, this) .onSync('blurred', this._onBlurred, this) .onSync('enterKeyed', this._onEnterKeyed, this) .onSync('tabKeyed', this._onTabKeyed, this) .onSync('escKeyed', this._onEscKeyed, this) .onSync('upKeyed', this._onUpKeyed, this) .onSync('downKeyed', this._onDownKeyed, this) .onSync('leftKeyed', this._onLeftKeyed, this) .onSync('rightKeyed', this._onRightKeyed, this) .onSync('queryChanged', this._onQueryChanged, this) .onSync('whitespaceChanged', this._onWhitespaceChanged, this); this._bindKeyboardShortcuts(o); this._setLanguageDirection(); } // instance methods // ---------------- _.mixin(Typeahead.prototype, { // ### private _bindKeyboardShortcuts: function(options) { if (!options.keyboardShortcuts) { return; } var $input = this.$input; var keyboardShortcuts = []; _.each(options.keyboardShortcuts, function(key) { if (typeof key === 'string') { key = key.toUpperCase().charCodeAt(0); } keyboardShortcuts.push(key); }); DOM.element(document).keydown(function(event) { var elt = (event.target || event.srcElement); var tagName = elt.tagName; if (elt.isContentEditable || tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA') { // already in an input return; } var which = event.which || event.keyCode; if (keyboardShortcuts.indexOf(which) === -1) { // not the right shortcut return; } $input.focus(); event.stopPropagation(); event.preventDefault(); }); }, _onSuggestionClicked: function onSuggestionClicked(type, $el) { var datum; var context = {selectionMethod: 'click'}; if (datum = this.dropdown.getDatumForSuggestion($el)) { this._select(datum, context); } }, _onCursorMoved: function onCursorMoved(event, updateInput) { var datum = this.dropdown.getDatumForCursor(); var currentCursorId = this.dropdown.getCurrentCursor().attr('id'); this.input.setActiveDescendant(currentCursorId); if (datum) { if (updateInput) { this.input.setInputValue(datum.value, true); } this.eventBus.trigger('cursorchanged', datum.raw, datum.datasetName); } }, _onCursorRemoved: function onCursorRemoved() { this.input.resetInputValue(); this._updateHint(); this.eventBus.trigger('cursorremoved'); }, _onDatasetRendered: function onDatasetRendered() { this._updateHint(); this.eventBus.trigger('updated'); }, _onOpened: function onOpened() { this._updateHint(); this.input.expand(); this.eventBus.trigger('opened'); }, _onEmpty: function onEmpty() { this.eventBus.trigger('empty'); }, _onRedrawn: function onRedrawn() { this.$node.css('top', 0 + 'px'); this.$node.css('left', 0 + 'px'); var inputRect = this.$input[0].getBoundingClientRect(); if (this.autoWidth) { this.$node.css('width', inputRect.width + 'px'); } var wrapperRect = this.$node[0].getBoundingClientRect(); var top = inputRect.bottom - wrapperRect.top; this.$node.css('top', top + 'px'); var left = inputRect.left - wrapperRect.left; this.$node.css('left', left + 'px'); this.eventBus.trigger('redrawn'); }, _onShown: function onShown() { this.eventBus.trigger('shown'); if (this.autoselect) { this.dropdown.cursorTopSuggestion(); } }, _onClosed: function onClosed() { this.input.clearHint(); this.input.removeActiveDescendant(); this.input.collapse(); this.eventBus.trigger('closed'); }, _onFocused: function onFocused() { this.isActivated = true; if (this.openOnFocus) { var query = this.input.getQuery(); if (query.length >= this.minLength) { this.dropdown.update(query); } else { this.dropdown.empty(); } this.dropdown.open(); } }, _onBlurred: function onBlurred() { var cursorDatum; var topSuggestionDatum; cursorDatum = this.dropdown.getDatumForCursor(); topSuggestionDatum = this.dropdown.getDatumForTopSuggestion(); var context = {selectionMethod: 'blur'}; if (!this.debug) { if (this.autoselectOnBlur && cursorDatum) { this._select(cursorDatum, context); } else if (this.autoselectOnBlur && topSuggestionDatum) { this._select(topSuggestionDatum, context); } else { this.isActivated = false; this.dropdown.empty(); this.dropdown.close(); } } }, _onEnterKeyed: function onEnterKeyed(type, $e) { var cursorDatum; var topSuggestionDatum; cursorDatum = this.dropdown.getDatumForCursor(); topSuggestionDatum = this.dropdown.getDatumForTopSuggestion(); var context = {selectionMethod: 'enterKey'}; if (cursorDatum) { this._select(cursorDatum, context); $e.preventDefault(); } else if (this.autoselect && topSuggestionDatum) { this._select(topSuggestionDatum, context); $e.preventDefault(); } }, _onTabKeyed: function onTabKeyed(type, $e) { if (!this.tabAutocomplete) { // Closing the dropdown enables further tabbing this.dropdown.close(); return; } var datum; var context = {selectionMethod: 'tabKey'}; if (datum = this.dropdown.getDatumForCursor()) { this._select(datum, context); $e.preventDefault(); } else { this._autocomplete(true); } }, _onEscKeyed: function onEscKeyed() { this.dropdown.close(); this.input.resetInputValue(); }, _onUpKeyed: function onUpKeyed() { var query = this.input.getQuery(); if (this.dropdown.isEmpty && query.length >= this.minLength) { this.dropdown.update(query); } else { this.dropdown.moveCursorUp(); } this.dropdown.open(); }, _onDownKeyed: function onDownKeyed() { var query = this.input.getQuery(); if (this.dropdown.isEmpty && query.length >= this.minLength) { this.dropdown.update(query); } else { this.dropdown.moveCursorDown(); } this.dropdown.open(); }, _onLeftKeyed: function onLeftKeyed() { if (this.dir === 'rtl') { this._autocomplete(); } }, _onRightKeyed: function onRightKeyed() { if (this.dir === 'ltr') { this._autocomplete(); } }, _onQueryChanged: function onQueryChanged(e, query) { this.input.clearHintIfInvalid(); if (query.length >= this.minLength) { this.dropdown.update(query); } else { this.dropdown.empty(); } this.dropdown.open(); this._setLanguageDirection(); }, _onWhitespaceChanged: function onWhitespaceChanged() { this._updateHint(); this.dropdown.open(); }, _setLanguageDirection: function setLanguageDirection() { var dir = this.input.getLanguageDirection(); if (this.dir !== dir) { this.dir = dir; this.$node.css('direction', dir); this.dropdown.setLanguageDirection(dir); } }, _updateHint: function updateHint() { var datum; var val; var query; var escapedQuery; var frontMatchRegEx; var match; datum = this.dropdown.getDatumForTopSuggestion(); if (datum && this.dropdown.isVisible() && !this.input.hasOverflow()) { val = this.input.getInputValue(); query = Input.normalizeQuery(val); escapedQuery = _.escapeRegExChars(query); // match input value, then capture trailing text frontMatchRegEx = new RegExp('^(?:' + escapedQuery + ')(.+$)', 'i'); match = frontMatchRegEx.exec(datum.value); // clear hint if there's no trailing text if (match) { this.input.setHint(val + match[1]); } else { this.input.clearHint(); } } else { this.input.clearHint(); } }, _autocomplete: function autocomplete(laxCursor) { var hint; var query; var isCursorAtEnd; var datum; hint = this.input.getHint(); query = this.input.getQuery(); isCursorAtEnd = laxCursor || this.input.isCursorAtEnd(); if (hint && query !== hint && isCursorAtEnd) { datum = this.dropdown.getDatumForTopSuggestion(); if (datum) { this.input.setInputValue(datum.value); } this.eventBus.trigger('autocompleted', datum.raw, datum.datasetName); } }, _select: function select(datum, context) { if (typeof datum.value !== 'undefined') { this.input.setQuery(datum.value); } if (this.clearOnSelected) { this.setVal(''); } else { this.input.setInputValue(datum.value, true); } this._setLanguageDirection(); var event = this.eventBus.trigger('selected', datum.raw, datum.datasetName, context); if (event.isDefaultPrevented() === false) { this.dropdown.close(); // #118: allow click event to bubble up to the body before removing // the suggestions otherwise we break event delegation _.defer(_.bind(this.dropdown.empty, this.dropdown)); } }, // ### public open: function open() { // if the menu is not activated yet, we need to update // the underlying dropdown menu to trigger the search // otherwise we're not gonna see anything if (!this.isActivated) { var query = this.input.getInputValue(); if (query.length >= this.minLength) { this.dropdown.update(query); } else { this.dropdown.empty(); } } this.dropdown.open(); }, close: function close() { this.dropdown.close(); }, setVal: function setVal(val) { // expect val to be a string, so be safe, and coerce val = _.toStr(val); if (this.isActivated) { this.input.setInputValue(val); } else { this.input.setQuery(val); this.input.setInputValue(val, true); } this._setLanguageDirection(); }, getVal: function getVal() { return this.input.getQuery(); }, destroy: function destroy() { this.input.destroy(); this.dropdown.destroy(); destroyDomStructure(this.$node, this.cssClasses); this.$node = null; }, getWrapper: function getWrapper() { return this.dropdown.$container[0]; } }); function buildDom(options) { var $input; var $wrapper; var $dropdown; var $hint; $input = DOM.element(options.input); $wrapper = DOM .element(html.wrapper.replace('%ROOT%', options.cssClasses.root)) .css(options.css.wrapper); // override the display property with the table-cell value // if the parent element is a table and the original input was a block // -> https://github.com/algolia/autocomplete.js/issues/16 if (!options.appendTo && $input.css('display') === 'block' && $input.parent().css('display') === 'table') { $wrapper.css('display', 'table-cell'); } var dropdownHtml = html.dropdown. replace('%PREFIX%', options.cssClasses.prefix). replace('%DROPDOWN_MENU%', options.cssClasses.dropdownMenu); $dropdown = DOM.element(dropdownHtml) .css(options.css.dropdown) .attr({ role: 'listbox', id: options.listboxId }); if (options.templates && options.templates.dropdownMenu) { $dropdown.html(_.templatify(options.templates.dropdownMenu)()); } $hint = $input.clone().css(options.css.hint).css(getBackgroundStyles($input)); $hint .val('') .addClass(_.className(options.cssClasses.prefix, options.cssClasses.hint, true)) .removeAttr('id name placeholder required') .prop('readonly', true) .attr({ 'aria-hidden': 'true', autocomplete: 'off', spellcheck: 'false', tabindex: -1 }); if ($hint.removeData) { $hint.removeData(); } // store the original values of the attrs that get modified // so modifications can be reverted on destroy $input.data(attrsKey, { 'aria-autocomplete': $input.attr('aria-autocomplete'), 'aria-expanded': $input.attr('aria-expanded'), 'aria-owns': $input.attr('aria-owns'), autocomplete: $input.attr('autocomplete'), dir: $input.attr('dir'), role: $input.attr('role'), spellcheck: $input.attr('spellcheck'), style: $input.attr('style'), type: $input.attr('type') }); $input .addClass(_.className(options.cssClasses.prefix, options.cssClasses.input, true)) .attr({ autocomplete: 'off', spellcheck: false, // Accessibility features // Give the field a presentation of a "select". // Combobox is the combined presentation of a single line textfield // with a listbox popup. // https://www.w3.org/WAI/PF/aria/roles#combobox role: 'combobox', // Let the screen reader know the field has an autocomplete // feature to it. 'aria-autocomplete': (options.datasets && options.datasets[0] && options.datasets[0].displayKey ? 'both' : 'list'), // Indicates whether the dropdown it controls is currently expanded or collapsed 'aria-expanded': 'false', 'aria-label': options.ariaLabel, // Explicitly point to the listbox, // which is a list of suggestions (aka options) 'aria-owns': options.listboxId }) .css(options.hint ? options.css.input : options.css.inputWithNoHint); // ie7 does not like it when dir is set to auto try { if (!$input.attr('dir')) { $input.attr('dir', 'auto'); } } catch (e) { // ignore } $wrapper = options.appendTo ? $wrapper.appendTo(DOM.element(options.appendTo).eq(0)).eq(0) : $input.wrap($wrapper).parent(); $wrapper .prepend(options.hint ? $hint : null) .append($dropdown); return { wrapper: $wrapper, input: $input, hint: $hint, menu: $dropdown }; } function getBackgroundStyles($el) { return { backgroundAttachment: $el.css('background-attachment'), backgroundClip: $el.css('background-clip'), backgroundColor: $el.css('background-color'), backgroundImage: $el.css('background-image'), backgroundOrigin: $el.css('background-origin'), backgroundPosition: $el.css('background-position'), backgroundRepeat: $el.css('background-repeat'), backgroundSize: $el.css('background-size') }; } function destroyDomStructure($node, cssClasses) { var $input = $node.find(_.className(cssClasses.prefix, cssClasses.input)); // need to remove attrs that weren't previously defined and // revert attrs that originally had a value _.each($input.data(attrsKey), function(val, key) { if (val === undefined) { $input.removeAttr(key); } else { $input.attr(key, val); } }); $input .detach() .removeClass(_.className(cssClasses.prefix, cssClasses.input, true)) .insertAfter($node); if ($input.removeData) { $input.removeData(attrsKey); } $node.remove(); } Typeahead.Dropdown = Dropdown; Typeahead.Input = Input; Typeahead.sources = require('../sources/index.js'); module.exports = Typeahead; ================================================ FILE: js/autocomplete.js/src/common/dom.js ================================================ 'use strict'; module.exports = { element: null }; ================================================ FILE: js/autocomplete.js/src/common/parseAlgoliaClientVersion.js ================================================ 'use strict'; module.exports = function parseAlgoliaClientVersion(agent) { var parsed = // User agent for algoliasearch >= 3.33.0 agent.match(/Algolia for JavaScript \((\d+\.)(\d+\.)(\d+)\)/) || // User agent for algoliasearch < 3.33.0 agent.match(/Algolia for vanilla JavaScript (\d+\.)(\d+\.)(\d+)/); if (parsed) { return [parsed[1], parsed[2], parsed[3]]; } return undefined; }; ================================================ FILE: js/autocomplete.js/src/common/utils.js ================================================ 'use strict'; var DOM = require('./dom.js'); function escapeRegExp(str) { return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); } module.exports = { // those methods are implemented differently // depending on which build it is, using // $... or angular... or Zepto... or require(...) isArray: null, isFunction: null, isObject: null, bind: null, each: null, map: null, mixin: null, isMsie: function(agentString) { if (agentString === undefined) { agentString = navigator.userAgent; } // from https://github.com/ded/bowser/blob/master/bowser.js if ((/(msie|trident)/i).test(agentString)) { var match = agentString.match(/(msie |rv:)(\d+(.\d+)?)/i); if (match) { return match[2]; } } return false; }, // http://stackoverflow.com/a/6969486 escapeRegExChars: function(str) { return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); }, isNumber: function(obj) { return typeof obj === 'number'; }, toStr: function toStr(s) { return s === undefined || s === null ? '' : s + ''; }, cloneDeep: function cloneDeep(obj) { var clone = this.mixin({}, obj); var self = this; this.each(clone, function(value, key) { if (value) { if (self.isArray(value)) { clone[key] = [].concat(value); } else if (self.isObject(value)) { clone[key] = self.cloneDeep(value); } } }); return clone; }, error: function(msg) { throw new Error(msg); }, every: function(obj, test) { var result = true; if (!obj) { return result; } this.each(obj, function(val, key) { if (result) { result = test.call(null, val, key, obj) && result; } }); return !!result; }, any: function(obj, test) { var found = false; if (!obj) { return found; } this.each(obj, function(val, key) { if (test.call(null, val, key, obj)) { found = true; return false; } }); return found; }, getUniqueId: (function() { var counter = 0; return function() { return counter++; }; })(), templatify: function templatify(obj) { if (this.isFunction(obj)) { return obj; } var $template = DOM.element(obj); if ($template.prop('tagName') === 'SCRIPT') { return function template() { return $template.text(); }; } return function template() { return String(obj); }; }, defer: function(fn) { setTimeout(fn, 0); }, noop: function() {}, formatPrefix: function(prefix, noPrefix) { return noPrefix ? '' : prefix + '-'; }, className: function(prefix, clazz, skipDot) { return (skipDot ? '' : '.') + prefix + clazz; }, escapeHighlightedString: function(str, highlightPreTag, highlightPostTag) { highlightPreTag = highlightPreTag || ''; var pre = document.createElement('div'); pre.appendChild(document.createTextNode(highlightPreTag)); highlightPostTag = highlightPostTag || ''; var post = document.createElement('div'); post.appendChild(document.createTextNode(highlightPostTag)); var div = document.createElement('div'); div.appendChild(document.createTextNode(str)); return div.innerHTML .replace(RegExp(escapeRegExp(pre.innerHTML), 'g'), highlightPreTag) .replace(RegExp(escapeRegExp(post.innerHTML), 'g'), highlightPostTag); } }; ================================================ FILE: js/autocomplete.js/src/jquery/plugin.js ================================================ 'use strict'; // setup DOM element var DOM = require('../common/dom.js'); var $ = require('jquery'); DOM.element = $; // setup utils functions var _ = require('../common/utils.js'); _.isArray = $.isArray; _.isFunction = $.isFunction; _.isObject = $.isPlainObject; _.bind = $.proxy; _.each = function(collection, cb) { // stupid argument order for jQuery.each $.each(collection, reverseArgs); function reverseArgs(index, value) { return cb(value, index); } }; _.map = $.map; _.mixin = $.extend; _.Event = $.Event; var Typeahead = require('../autocomplete/typeahead.js'); var EventBus = require('../autocomplete/event_bus.js'); var old; var typeaheadKey; var methods; old = $.fn.autocomplete; typeaheadKey = 'aaAutocomplete'; methods = { // supported signatures: // function(o, dataset, dataset, ...) // function(o, [dataset, dataset, ...]) initialize: function initialize(o, datasets) { datasets = _.isArray(datasets) ? datasets : [].slice.call(arguments, 1); o = o || {}; return this.each(attach); function attach() { var $input = $(this); var eventBus = new EventBus({el: $input}); var typeahead; typeahead = new Typeahead({ input: $input, eventBus: eventBus, dropdownMenuContainer: o.dropdownMenuContainer, hint: o.hint === undefined ? true : !!o.hint, minLength: o.minLength, autoselect: o.autoselect, autoselectOnBlur: o.autoselectOnBlur, tabAutocomplete: o.tabAutocomplete, openOnFocus: o.openOnFocus, templates: o.templates, debug: o.debug, clearOnSelected: o.clearOnSelected, cssClasses: o.cssClasses, datasets: datasets, keyboardShortcuts: o.keyboardShortcuts, appendTo: o.appendTo, autoWidth: o.autoWidth }); $input.data(typeaheadKey, typeahead); } }, open: function open() { return this.each(openTypeahead); function openTypeahead() { var $input = $(this); var typeahead; if (typeahead = $input.data(typeaheadKey)) { typeahead.open(); } } }, close: function close() { return this.each(closeTypeahead); function closeTypeahead() { var $input = $(this); var typeahead; if (typeahead = $input.data(typeaheadKey)) { typeahead.close(); } } }, val: function val(newVal) { // mirror jQuery#val functionality: read operate on first match, // write operates on all matches return !arguments.length ? getVal(this.first()) : this.each(setVal); function setVal() { var $input = $(this); var typeahead; if (typeahead = $input.data(typeaheadKey)) { typeahead.setVal(newVal); } } function getVal($input) { var typeahead; var query; if (typeahead = $input.data(typeaheadKey)) { query = typeahead.getVal(); } return query; } }, destroy: function destroy() { return this.each(unattach); function unattach() { var $input = $(this); var typeahead; if (typeahead = $input.data(typeaheadKey)) { typeahead.destroy(); $input.removeData(typeaheadKey); } } } }; $.fn.autocomplete = function(method) { var tts; // methods that should only act on intialized typeaheads if (methods[method] && method !== 'initialize') { // filter out non-typeahead inputs tts = this.filter(function() { return !!$(this).data(typeaheadKey); }); return methods[method].apply(tts, [].slice.call(arguments, 1)); } return methods.initialize.apply(this, arguments); }; $.fn.autocomplete.noConflict = function noConflict() { $.fn.autocomplete = old; return this; }; $.fn.autocomplete.sources = Typeahead.sources; $.fn.autocomplete.escapeHighlightedString = _.escapeHighlightedString; module.exports = $.fn.autocomplete; ================================================ FILE: js/autocomplete.js/src/sources/hits.js ================================================ 'use strict'; var _ = require('../common/utils.js'); var version = require('../../version.js'); var parseAlgoliaClientVersion = require('../common/parseAlgoliaClientVersion.js'); function createMultiQuerySource() { var queries = []; var lastResults = []; var lastSearch = window.Promise.resolve(); function requestSearch(queryClient, queryIndex) { // Since all requests happen synchronously, this is executed once all the // sources have been requested. return window.Promise.resolve() .then(function() { if (queries.length) { lastSearch = queryClient.search(queries); queries = []; } return lastSearch; }) .then(function(result) { if (!result) { return undefined; } lastResults = result.results; return lastResults[queryIndex]; }); } return function multiQuerySource(searchIndex, params) { return function search(query, cb) { var queryClient = searchIndex.as; var queryIndex = queries.push({ indexName: searchIndex.indexName, query: query, params: params }) - 1; requestSearch(queryClient, queryIndex) .then(function(result) { if (result) { cb(result.hits, result); } }) .catch(function(error) { _.error(error.message); }); }; }; } var source = createMultiQuerySource(); module.exports = function search(index, params) { var algoliaVersion = parseAlgoliaClientVersion(index.as._ua); if (algoliaVersion && algoliaVersion[0] >= 3 && algoliaVersion[1] > 20) { var autocompleteUserAgent = 'autocomplete.js ' + version; if (index.as._ua.indexOf(autocompleteUserAgent) === -1) { index.as._ua += '; ' + autocompleteUserAgent; } } return source(index, params); }; ================================================ FILE: js/autocomplete.js/src/sources/index.js ================================================ 'use strict'; module.exports = { hits: require('./hits.js'), popularIn: require('./popularIn.js') }; ================================================ FILE: js/autocomplete.js/src/sources/popularIn.js ================================================ 'use strict'; var _ = require('../common/utils.js'); var version = require('../../version.js'); var parseAlgoliaClientVersion = require('../common/parseAlgoliaClientVersion.js'); module.exports = function popularIn(index, params, details, options) { var algoliaVersion = parseAlgoliaClientVersion(index.as._ua); if (algoliaVersion && algoliaVersion[0] >= 3 && algoliaVersion[1] > 20) { params = params || {}; params.additionalUA = 'autocomplete.js ' + version; } if (!details.source) { return _.error("Missing 'source' key"); } var source = _.isFunction(details.source) ? details.source : function(hit) { return hit[details.source]; }; if (!details.index) { return _.error("Missing 'index' key"); } var detailsIndex = details.index; options = options || {}; return sourceFn; function sourceFn(query, cb) { index.search(query, params, function(error, content) { if (error) { _.error(error.message); return; } if (content.hits.length > 0) { var first = content.hits[0]; var detailsParams = _.mixin({hitsPerPage: 0}, details); delete detailsParams.source; // not a query parameter delete detailsParams.index; // not a query parameter var detailsAlgoliaVersion = parseAlgoliaClientVersion(detailsIndex.as._ua); if (detailsAlgoliaVersion && detailsAlgoliaVersion[0] >= 3 && detailsAlgoliaVersion[1] > 20) { params.additionalUA = 'autocomplete.js ' + version; } detailsIndex.search(source(first), detailsParams, function(error2, content2) { if (error2) { _.error(error2.message); return; } var suggestions = []; // add the 'all department' entry before others if (options.includeAll) { var label = options.allTitle || 'All departments'; suggestions.push(_.mixin({ facet: {value: label, count: content2.nbHits} }, _.cloneDeep(first))); } // enrich the first hit iterating over the facets _.each(content2.facets, function(values, facet) { _.each(values, function(count, value) { suggestions.push(_.mixin({ facet: {facet: facet, value: value, count: count} }, _.cloneDeep(first))); }); }); // append all other hits for (var i = 1; i < content.hits.length; ++i) { suggestions.push(content.hits[i]); } cb(suggestions, content); }); return; } cb([]); }); } }; ================================================ FILE: js/autocomplete.js/src/standalone/index.js ================================================ 'use strict'; // this will inject Zepto in window, unfortunately no easy commonJS zepto build var zepto = require('../../zepto.js'); // setup DOM element var DOM = require('../common/dom.js'); DOM.element = zepto; // setup utils functions var _ = require('../common/utils.js'); _.isArray = zepto.isArray; _.isFunction = zepto.isFunction; _.isObject = zepto.isPlainObject; _.bind = zepto.proxy; _.each = function(collection, cb) { // stupid argument order for jQuery.each zepto.each(collection, reverseArgs); function reverseArgs(index, value) { return cb(value, index); } }; _.map = zepto.map; _.mixin = zepto.extend; _.Event = zepto.Event; var typeaheadKey = 'aaAutocomplete'; var Typeahead = require('../autocomplete/typeahead.js'); var EventBus = require('../autocomplete/event_bus.js'); function autocomplete(selector, options, datasets, typeaheadObject) { datasets = _.isArray(datasets) ? datasets : [].slice.call(arguments, 2); var inputs = zepto(selector).each(function(i, input) { var $input = zepto(input); var eventBus = new EventBus({el: $input}); var typeahead = typeaheadObject || new Typeahead({ input: $input, eventBus: eventBus, dropdownMenuContainer: options.dropdownMenuContainer, hint: options.hint === undefined ? true : !!options.hint, minLength: options.minLength, autoselect: options.autoselect, autoselectOnBlur: options.autoselectOnBlur, tabAutocomplete: options.tabAutocomplete, openOnFocus: options.openOnFocus, templates: options.templates, debug: options.debug, clearOnSelected: options.clearOnSelected, cssClasses: options.cssClasses, datasets: datasets, keyboardShortcuts: options.keyboardShortcuts, appendTo: options.appendTo, autoWidth: options.autoWidth, ariaLabel: options.ariaLabel || input.getAttribute('aria-label') }); $input.data(typeaheadKey, typeahead); }); // expose all methods in the `autocomplete` attribute inputs.autocomplete = {}; _.each(['open', 'close', 'getVal', 'setVal', 'destroy', 'getWrapper'], function(method) { inputs.autocomplete[method] = function() { var methodArguments = arguments; var result; inputs.each(function(j, input) { var typeahead = zepto(input).data(typeaheadKey); result = typeahead[method].apply(typeahead, methodArguments); }); return result; }; }); return inputs; } autocomplete.sources = Typeahead.sources; autocomplete.escapeHighlightedString = _.escapeHighlightedString; var wasAutocompleteSet = 'autocomplete' in window; var oldAutocomplete = window.autocomplete; autocomplete.noConflict = function noConflict() { if (wasAutocompleteSet) { window.autocomplete = oldAutocomplete; } else { delete window.autocomplete; } return autocomplete; }; module.exports = autocomplete; ================================================ FILE: js/autocomplete.js/test/ci.sh ================================================ #!/usr/bin/env bash set -e # exit when error, no verbose if [ "$TEST_SUITE" == "unit" ]; then ./node_modules/karma/bin/karma start --single-run elif [ "$TRAVIS_SECURE_ENV_VARS" == "true" -a "$TEST_SUITE" == "integration" ]; then static -p 8080 & sleep 3 && ./node_modules/mocha/bin/mocha --harmony -R spec ./test/integration/test.js elif [ "$TEST_SUITE" == "examples" ]; then yarn docs:netlify else echo "Not running any tests" fi ================================================ FILE: js/autocomplete.js/test/fixtures.js ================================================ 'use strict'; var fixtures = {}; fixtures.data = { simple: [ {value: 'big'}, {value: 'bigger'}, {value: 'biggest'}, {value: 'small'}, {value: 'smaller'}, {value: 'smallest'} ], animals: [ {value: 'dog'}, {value: 'cat'}, {value: 'moose'} ] }; fixtures.html = { textInput: '', angularTextInput: '', input: '', hint: '', menu: '', customMenu: [ '' ].join(''), customMenuContainer: '
', dataset: [ '
', '', '

one

', '

two

', '

three

', '
', '
' ].join('') }; module.exports = fixtures; ================================================ FILE: js/autocomplete.js/test/helpers/mocks.js ================================================ 'use strict'; var _ = require('../../src/common/utils.js'); var $ = require('jquery'); module.exports = function mock(Constructor) { var constructorSpy; Mock.prototype = Constructor.prototype; constructorSpy = jasmine.createSpy('mock constructor').and.callFake(Mock); // copy instance methods for (var key in Constructor) { if (typeof Constructor[key] === 'function') { constructorSpy[key] = Constructor[key]; } } return constructorSpy; function Mock() { var instance = _.mixin({}, Constructor.prototype); for (var key in instance) { if (typeof instance[key] === 'function') { spyOn(instance, key); // special case for some components if (key === 'bind') { instance[key].and.callFake(function() { return this; }); } } } // have the event emitter methods call through instance.onSync && instance.onSync.and.callThrough(); instance.onAsync && instance.onAsync.and.callThrough(); instance.off && instance.off.and.callThrough(); instance.trigger && instance.trigger.and.callThrough(); // have some datasets methods call through instance.getRoot && instance.getRoot.and.callFake(function() { return $(''); }); instance.constructor = Constructor; return instance; } }; ================================================ FILE: js/autocomplete.js/test/helpers/waits_for.js ================================================ 'use strict'; var waitsForAndRuns = function(escapeFunction, runFunction, escapeTime) { // check the escapeFunction every millisecond so as soon as it is met we can escape the function var interval = setInterval(function() { if (escapeFunction()) { clearMe(); runFunction(); } }, 1); // in case we never reach the escapeFunction, we will time out // at the escapeTime var timeOut = setTimeout(function() { clearMe(); runFunction(); }, escapeTime); // clear the interval and the timeout function clearMe() { clearInterval(interval); clearTimeout(timeOut); } }; module.exports = waitsForAndRuns; ================================================ FILE: js/autocomplete.js/test/integration/test.html ================================================
================================================ FILE: js/autocomplete.js/test/integration/test.js ================================================ 'use strict'; /* eslint-env jasmine */ var wd = require('yiewd'); var colors = require('colors'); var expect = require('chai').expect; var f = require('util').format; var env = process.env; var browser; var caps; browser = (process.env.BROWSER || 'chrome').split(':'); caps = { name: f('[%s] typeahead.js ui', browser.join(' , ')), browserName: browser[0] }; setIf(caps, 'version', browser[1]); setIf(caps, 'platform', browser[2]); setIf(caps, 'tunnel-identifier', env['TRAVIS_JOB_NUMBER']); setIf(caps, 'build', env['TRAVIS_BUILD_NUMBER']); setIf(caps, 'tags', env['CI'] ? ['CI'] : ['local']); function setIf(obj, key, val) { val && (obj[key] = val); } describe('jquery-typeahead.js', function() { var driver; var body; var input; var hint; var dropdown; var allPassed = true; this.timeout(300000); before(function(done) { var host = 'ondemand.saucelabs.com'; var port = 80; var username; var password; if (env['CI']) { host = 'localhost'; port = 4445; username = env['SAUCE_USERNAME']; password = env['SAUCE_ACCESS_KEY']; } driver = wd.remote(host, port, username, password); driver.configureHttp({ timeout: 30000, retries: 5, retryDelay: 200 }); driver.on('status', function(info) { console.log(info.cyan); }); driver.on('command', function(meth, path, data) { console.log(' > ' + meth.yellow, path.grey, data || ''); }); driver.run(function*() { yield this.init(caps); yield this.get((env['TEST_HOST'] || 'http://localhost:8080') + '/test/integration/test.html'); body = this.elementByTagName('body'); input = yield this.elementById('states'); hint = yield this.elementByClassName('aa-hint'); dropdown = yield this.elementByClassName('aa-dropdown-menu'); done(); }); }); beforeEach(function(done) { driver.run(function*() { yield body.click(); yield this.execute('$("#states").autocomplete("val", "")'); done(); }); }); afterEach(function() { allPassed = allPassed && (this.currentTest.state === 'passed'); }); after(function(done) { driver.run(function*() { yield this.quit(); yield driver.sauceJobStatus(allPassed); done(); }); }); function testSuite () { describe('on blur', function() { it('should close dropdown', function(done) { driver.run(function*() { yield input.click(); yield input.type('mi'); expect(yield dropdown.isDisplayed()).to.equal(true); yield body.click(); expect(yield dropdown.isDisplayed()).to.equal(false); done(); }); }); it('should clear hint', function(done) { driver.run(function*() { yield input.click(); yield input.type('mi'); expect(yield hint.getValue()).to.equal('michigan'); yield body.click(); expect(yield hint.getValue()).to.equal(''); done(); }); }); }); describe('on query change', function() { it('should open dropdown if suggestions', function(done) { driver.run(function*() { yield input.click(); yield input.type('mi'); expect(yield dropdown.isDisplayed()).to.equal(true); done(); }); }); it('should position the dropdown correctly', function(done) { driver.run(function*() { yield input.click(); yield input.type('mi'); var inputPos = yield input.getLocation(); var dropdownPos = yield dropdown.getLocation(); expect(inputPos.x >= dropdownPos.x - 2 && inputPos.x <= dropdownPos.x + 2).to.equal(true); expect(dropdownPos.y > inputPos.y).to.equal(true); done(); }); }); it('should close dropdown if no suggestions', function(done) { driver.run(function*() { yield input.click(); yield input.type('huh?'); expect(yield dropdown.isDisplayed()).to.equal(false); done(); }); }); it('should render suggestions if suggestions', function(done) { driver.run(function*() { var suggestions; yield input.click(); yield input.type('mi'); suggestions = yield dropdown.elementsByClassName('aa-suggestion'); expect(suggestions).to.have.length('4'); expect(yield suggestions[0].text()).to.equal('Michigan'); expect(yield suggestions[1].text()).to.equal('Minnesota'); expect(yield suggestions[2].text()).to.equal('Mississippi'); expect(yield suggestions[3].text()).to.equal('Missouri'); done(); }); }); it('should show hint if top suggestion is a match', function(done) { driver.run(function*() { yield input.click(); yield input.type('mi'); expect(yield hint.getValue()).to.equal('michigan'); done(); }); }); it('should not show hint if top suggestion is not a match', function(done) { driver.run(function*() { yield input.click(); yield input.type('ham'); expect(yield hint.getValue()).to.equal(''); done(); }); }); it('should not show hint if there is query overflow', function(done) { driver.run(function*() { yield input.click(); yield input.type('this is a very long value so deal with it otherwise'); expect(yield hint.getValue()).to.equal(''); done(); }); }); }); describe('on up arrow', function() { it('should cycle through suggestions', function(done) { driver.run(function*() { var suggestions; yield input.click(); yield input.type('mi'); suggestions = yield dropdown.elementsByClassName('aa-suggestion'); yield input.type(wd.SPECIAL_KEYS['Up arrow']); expect(yield input.getValue()).to.equal('Missouri'); expect(yield suggestions[3].getAttribute('class')).to.equal('aa-suggestion aa-cursor'); yield input.type(wd.SPECIAL_KEYS['Up arrow']); expect(yield input.getValue()).to.equal('Mississippi'); expect(yield suggestions[2].getAttribute('class')).to.equal('aa-suggestion aa-cursor'); yield input.type(wd.SPECIAL_KEYS['Up arrow']); expect(yield input.getValue()).to.equal('Minnesota'); expect(yield suggestions[1].getAttribute('class')).to.equal('aa-suggestion aa-cursor'); yield input.type(wd.SPECIAL_KEYS['Up arrow']); expect(yield input.getValue()).to.equal('Michigan'); expect(yield suggestions[0].getAttribute('class')).to.equal('aa-suggestion aa-cursor'); yield input.type(wd.SPECIAL_KEYS['Up arrow']); expect(yield input.getValue()).to.equal('mi'); expect(yield suggestions[0].getAttribute('class')).to.equal('aa-suggestion'); expect(yield suggestions[1].getAttribute('class')).to.equal('aa-suggestion'); expect(yield suggestions[2].getAttribute('class')).to.equal('aa-suggestion'); expect(yield suggestions[3].getAttribute('class')).to.equal('aa-suggestion'); done(); }); }); }); describe('on down arrow', function() { it('should cycle through suggestions', function(done) { driver.run(function*() { var suggestions; yield input.click(); yield input.type('mi'); suggestions = yield dropdown.elementsByClassName('aa-suggestion'); yield input.type(wd.SPECIAL_KEYS['Down arrow']); expect(yield input.getValue()).to.equal('Michigan'); expect(yield suggestions[0].getAttribute('class')).to.equal('aa-suggestion aa-cursor'); yield input.type(wd.SPECIAL_KEYS['Down arrow']); expect(yield input.getValue()).to.equal('Minnesota'); expect(yield suggestions[1].getAttribute('class')).to.equal('aa-suggestion aa-cursor'); yield input.type(wd.SPECIAL_KEYS['Down arrow']); expect(yield input.getValue()).to.equal('Mississippi'); expect(yield suggestions[2].getAttribute('class')).to.equal('aa-suggestion aa-cursor'); yield input.type(wd.SPECIAL_KEYS['Down arrow']); expect(yield input.getValue()).to.equal('Missouri'); expect(yield suggestions[3].getAttribute('class')).to.equal('aa-suggestion aa-cursor'); yield input.type(wd.SPECIAL_KEYS['Down arrow']); expect(yield input.getValue()).to.equal('mi'); expect(yield suggestions[0].getAttribute('class')).to.equal('aa-suggestion'); expect(yield suggestions[1].getAttribute('class')).to.equal('aa-suggestion'); expect(yield suggestions[2].getAttribute('class')).to.equal('aa-suggestion'); expect(yield suggestions[3].getAttribute('class')).to.equal('aa-suggestion'); done(); }); }); }); describe('on escape', function() { it('should close dropdown', function(done) { driver.run(function*() { yield input.click(); yield input.type('mi'); expect(yield dropdown.isDisplayed()).to.equal(true); yield input.type(wd.SPECIAL_KEYS['Escape']); expect(yield dropdown.isDisplayed()).to.equal(false); done(); }); }); it('should clear hint', function(done) { driver.run(function*() { yield input.click(); yield input.type('mi'); expect(yield hint.getValue()).to.equal('michigan'); yield input.type(wd.SPECIAL_KEYS['Escape']); expect(yield hint.getValue()).to.equal(''); done(); }); }); }); describe('on tab', function() { it('should autocomplete if hint is present', function(done) { driver.run(function*() { yield input.click(); yield input.type('mi'); yield input.type(wd.SPECIAL_KEYS['Tab']); expect(yield input.getValue()).to.equal('Michigan'); done(); }); }); it('should select if cursor is on suggestion', function(done) { driver.run(function*() { var suggestions; yield input.click(); yield input.type('mi'); suggestions = yield dropdown.elementsByClassName('aa-suggestion'); yield input.type(wd.SPECIAL_KEYS['Down arrow']); yield input.type(wd.SPECIAL_KEYS['Down arrow']); yield input.type(wd.SPECIAL_KEYS['Tab']); expect(yield dropdown.isDisplayed()).to.equal(false); expect(yield input.getValue()).to.equal('Minnesota'); done(); }); }); }); describe('on right arrow', function() { it('should autocomplete if hint is present', function(done) { driver.run(function*() { yield input.click(); yield input.type('mi'); yield input.type(wd.SPECIAL_KEYS['Right arrow']); expect(yield input.getValue()).to.equal('Michigan'); done(); }); }); }); describe('on suggestion click', function() { it('should select suggestion', function(done) { if (browser[0] === 'firefox') { // crazy Firefox issue, skip it done(); return; } driver.run(function*() { var suggestions; yield input.click(); yield input.type('mi'); suggestions = yield dropdown.elementsByClassName('aa-suggestion'); yield suggestions[1].click(); expect(yield dropdown.isDisplayed()).to.equal(false); expect(yield input.getValue()).to.equal('Minnesota'); done(); }); }); }); describe('on enter', function() { it('should select if cursor is on suggestion', function(done) { driver.run(function*() { var suggestions; yield input.click(); yield input.type('mi'); suggestions = yield dropdown.elementsByClassName('aa-suggestion'); yield input.type(wd.SPECIAL_KEYS['Down arrow']); yield input.type(wd.SPECIAL_KEYS['Down arrow']); yield input.type(wd.SPECIAL_KEYS['Return']); expect(yield dropdown.isDisplayed()).to.equal(false); expect(yield input.getValue()).to.equal('Minnesota'); done(); }); }); }); } testSuite(); describe('appendTo', function () { before('all', function*() { yield this.execute("buildAutocomplete({hint: false, appendTo: 'body'})"); }); testSuite(); }); }); ================================================ FILE: js/autocomplete.js/test/playground.css ================================================ .my-custom-menu { width: 400px; } .aa-cursor { background: #F6624E; color: white; } .autocomplete-wrapper { display: block; margin: 50px 0; } .aa-dropdown-menu { min-width: 100%; border: 1px solid #ccc; background-color: white; padding: 4px; } .aa-suggestion { padding: 5px 10px; } .aa-suggestion em{ font-style: normal; font-weight: bold; } .aa-info-results { padding: 4px; color: #aaa; font-style: italic; text-align: right; } .aa-category-title{ display: block; width: 100%; padding: 8px 4px 4px; border-bottom: solid 1px #eee; font-size: .8em; color: #F6624E; font-weight: bold; text-transform: uppercase; } .aa-dropdown-footer{ padding: 10px 12px 6px 12px; margin-top: 12px; border-top: solid 1px #eee; text-align: right; color: #aaa; font-size: 12px; letter-spacing: 1px; } ================================================ FILE: js/autocomplete.js/test/playground.html ================================================

Simple auto-complete

Go

Multi-sections auto-complete

Go

Simple auto-complete with debounce

Go

Multi-indexes auto-complete

Go
================================================ FILE: js/autocomplete.js/test/playground_angular.html ================================================

Simple auto-complete

Go
================================================ FILE: js/autocomplete.js/test/playground_jquery.html ================================================

Simple auto-complete

Go

Multi-sections auto-complete

Tabbed auto-complete

Go
================================================ FILE: js/autocomplete.js/test/test.bundle.js ================================================ 'use strict'; var context = require.context('.', true, /.+_spec\.js$/); context.keys().forEach(context); module.exports = context; ================================================ FILE: js/autocomplete.js/test/unit/angular_spec.js ================================================ 'use strict'; /* eslint-env mocha, jasmine */ if (typeof Function.prototype.bind != 'function') { Function.prototype.bind = function bind(obj) { var args = Array.prototype.slice.call(arguments, 1), self = this, nop = function() { }, bound = function() { return self.apply( this instanceof nop ? this : (obj || {}), args.concat( Array.prototype.slice.call(arguments) ) ); }; nop.prototype = this.prototype || {}; bound.prototype = new nop(); return bound; }; } describe('autocomplete directive', function() { global.jQuery = require('jquery'); var fixtures = require('../fixtures.js'); var angular = require('angular'); require('../../src/angular/directive.js'); require('angular-mocks'); var scope; beforeEach(angular.mock.module('algolia.autocomplete')); describe('with scope', function() { beforeEach(angular.mock.inject(function($rootScope, $compile) { scope = $rootScope.$new(); scope.q = ''; scope.getDatasets = function() { return []; }; })); describe('when initialized', function() { var form; beforeEach(function() { inject(function($compile) { form = $compile(fixtures.html.angularTextInput)(scope); }); scope.$digest(); }); it('should have a parent', function() { expect(form.parent().length).toEqual(1); }); }); }); afterAll(function() { global.jQuery = undefined; }); }); ================================================ FILE: js/autocomplete.js/test/unit/dataset_spec.js ================================================ 'use strict'; /* eslint-env mocha, jasmine */ describe('Dataset', function() { require('../../src/common/dom.js').element = require('jquery'); require('../../src/jquery/plugin.js'); var $ = require('jquery'); require('jasmine-jquery'); var Dataset = require('../../src/autocomplete/dataset.js'); beforeEach(function() { this.dataset = new Dataset({ name: 'test', source: this.source = jasmine.createSpy('source') }); }); it('should throw an error if source is missing', function() { expect(noSource).toThrow(); function noSource() { new Dataset(); } }); it('should throw an error if the name is not a valid class name', function() { expect(fn).toThrow(); function fn() { var d = new Dataset({name: 'a space', source: $.noop}); } }); describe('#getRoot', function() { it('should return the root element', function() { expect(this.dataset.getRoot()).toBeMatchedBy('div.aa-dataset-test'); }); }); describe('#update', function() { it('should render suggestions', function() { this.source.and.callFake(fakeGetWithSyncResults); this.dataset.update('woah'); expect(this.dataset.getRoot()).toContainText('one'); expect(this.dataset.getRoot()).toContainText('two'); expect(this.dataset.getRoot()).toContainText('three'); }); it('should allow custom display functions', function() { this.dataset = new Dataset({ name: 'test', display: function(o) { return o.display; }, source: this.source = jasmine.createSpy('source') }); this.source.and.callFake(fakeGetForDisplayFn); this.dataset.update('woah'); expect(this.dataset.getRoot()).toContainText('4'); expect(this.dataset.getRoot()).toContainText('5'); expect(this.dataset.getRoot()).toContainText('6'); }); it('should render empty when no suggestions are available', function() { this.dataset = new Dataset({ source: this.source, templates: { empty: '

empty

' } }); this.source.and.callFake(fakeGetWithSyncEmptyResults); this.dataset.update('woah'); expect(this.dataset.getRoot()).toContainText('empty'); }); it('should throw an error if suggestions is not an array', function() { this.source.and.callFake(fakeGetWithSyncNonArrayResults); expect(this.dataset.update.bind(this.dataset, 'woah')) .toThrowError(TypeError, 'suggestions must be an array'); }); it('should set the aa-without class when no suggestions are available', function() { var $menu = $('
'); this.dataset = new Dataset({ $menu: $menu, source: this.source, templates: { empty: '

empty

' } }); this.source.and.callFake(fakeGetWithSyncEmptyResults); this.dataset.update('woah'); expect($menu).toHaveClass('aa-without-1'); expect($menu).not.toHaveClass('aa-with-1'); }); it('should set the aa-with class when suggestions are available', function() { var $menu = $('
'); this.dataset = new Dataset({ $menu: $menu, name: 'fake', source: this.source, templates: { empty: '

empty

' } }); this.source.and.callFake(fakeGetWithSyncResults); this.dataset.update('woah'); expect($menu).not.toHaveClass('aa-without-fake'); expect($menu).toHaveClass('aa-with-fake'); }); it('should allow dataset name=0 and use the provided div', function() { var $menu = $('
'); this.dataset = new Dataset({ $menu: $menu, name: 0, source: this.source }); expect(this.dataset.$el).toHaveClass('predefined'); }); it('should render isEmpty with extra params', function() { var spy = jasmine.createSpy('empty with extra params'); this.dataset = new Dataset({ source: this.source, templates: { empty: spy } }); this.source.and.callFake(fakeGetWithSyncEmptyResultsAndExtraParams); this.dataset.update('woah'); expect(spy).toHaveBeenCalled(); expect(spy.calls.argsFor(0).length).toEqual(4); expect(spy.calls.argsFor(0)[0]).toEqual({query: 'woah', isEmpty: true}); expect(spy.calls.argsFor(0)[1]).toEqual(42); expect(spy.calls.argsFor(0)[2]).toEqual(true); expect(spy.calls.argsFor(0)[3]).toEqual(false); }); it('should render with extra params', function() { var headerSpy = jasmine.createSpy('header with extra params'); var footerSpy = jasmine.createSpy('footer with extra params'); var suggestionSpy = jasmine.createSpy('suggestion with extra params'); this.dataset = new Dataset({ source: this.source, templates: { header: headerSpy, footer: footerSpy, suggestion: suggestionSpy } }); var suggestions; fakeGetWithSyncResultsAndExtraParams('woah', function(all) { suggestions = all; }); this.source.and.callFake(fakeGetWithSyncResultsAndExtraParams); this.dataset.update('woah'); expect(headerSpy).toHaveBeenCalled(); expect(headerSpy.calls.argsFor(0).length).toEqual(4); expect(headerSpy.calls.argsFor(0)[0]).toEqual({query: 'woah', isEmpty: false}); expect(headerSpy.calls.argsFor(0)[1]).toEqual(42); expect(headerSpy.calls.argsFor(0)[2]).toEqual(true); expect(headerSpy.calls.argsFor(0)[3]).toEqual(false); expect(footerSpy).toHaveBeenCalled(); expect(footerSpy.calls.argsFor(0).length).toEqual(4); expect(footerSpy.calls.argsFor(0)[0]).toEqual({query: 'woah', isEmpty: false}); expect(footerSpy.calls.argsFor(0)[1]).toEqual(42); expect(footerSpy.calls.argsFor(0)[2]).toEqual(true); expect(footerSpy.calls.argsFor(0)[3]).toEqual(false); expect(suggestionSpy).toHaveBeenCalled(); for (var i = 0; i < 2; ++i) { expect(suggestionSpy.calls.argsFor(i).length).toEqual(4); expect(suggestionSpy.calls.argsFor(i)[0]).toEqual(suggestions[i]); expect(suggestionSpy.calls.argsFor(i)[1]).toEqual(42); expect(suggestionSpy.calls.argsFor(i)[2]).toEqual(true); expect(suggestionSpy.calls.argsFor(i)[3]).toEqual(false); } }); it('should render header', function() { this.dataset = new Dataset({ source: this.source, templates: { header: '

header

' } }); this.source.and.callFake(fakeGetWithSyncResults); this.dataset.update('woah'); expect(this.dataset.getRoot()).toContainText('header'); }); it('should render footer', function() { this.dataset = new Dataset({ source: this.source, templates: { footer: function(c) { return '

' + c.query + '

'; } } }); this.source.and.callFake(fakeGetWithSyncResults); this.dataset.update('woah'); expect(this.dataset.getRoot()).toContainText('woah'); }); it('should not render header/footer if there is no content', function() { this.dataset = new Dataset({ source: this.source, templates: { header: '

header

', footer: '

footer

' } }); this.source.and.callFake(fakeGetWithSyncEmptyResults); this.dataset.update('woah'); expect(this.dataset.getRoot()).not.toContainText('header'); expect(this.dataset.getRoot()).not.toContainText('footer'); }); it('should not render stale suggestions', function(done) { this.source.and.callFake(fakeGetWithAsyncResults); this.dataset.update('woah'); this.source.and.callFake(fakeGetWithSyncResults); this.dataset.update('nelly'); var that = this; setTimeout(function() { expect(that.dataset.getRoot()).toContainText('one'); expect(that.dataset.getRoot()).toContainText('two'); expect(that.dataset.getRoot()).toContainText('three'); expect(that.dataset.getRoot()).not.toContainText('four'); expect(that.dataset.getRoot()).not.toContainText('five'); done(); }, 100); }); it('should not render suggestions if update was canceled', function(done) { this.source.and.callFake(fakeGetWithAsyncResults); this.dataset.update('woah'); this.dataset.cancel(); var that = this; setTimeout(function() { expect(that.dataset.getRoot()).toBeEmpty(); done(); }, 100); }); it('should trigger rendered after suggestions are rendered', function(done) { var spy; this.dataset.onSync('rendered', spy = jasmine.createSpy()); this.source.and.callFake(fakeGetWithSyncResults); this.dataset.update('woah'); setTimeout(function() { expect(spy.calls.count()).toBe(1); done(); }, 100); }); it('should cache latest query, suggestions and extra render arguments', function() { this.source.and.callFake(fakeGetWithSyncResultsAndExtraParams); this.dataset.update('woah'); expect(this.dataset.cachedQuery).toEqual('woah'); expect(this.dataset.cachedSuggestions).toEqual([ {value: 'one', raw: {value: 'one'}}, {value: 'two', raw: {value: 'two'}}, {value: 'three', raw: {value: 'three'}} ]); expect(this.dataset.cachedRenderExtraArgs).toEqual([42, true, false]); }); it('should retrieve cached results for subsequent identical queries', function() { this.source.and.callFake(fakeGetWithSyncResults); this.dataset.update('woah'); expect(this.source.calls.count()).toBe(1); expect(this.dataset.getRoot()).toContainText('one'); expect(this.dataset.getRoot()).toContainText('two'); expect(this.dataset.getRoot()).toContainText('three'); this.dataset.clear(); this.dataset.update('woah'); expect(this.source.calls.count()).toBe(1); expect(this.dataset.getRoot()).toContainText('one'); expect(this.dataset.getRoot()).toContainText('two'); expect(this.dataset.getRoot()).toContainText('three'); }); it('should not retrieve cached results for subsequent identical queries if cache is disabled', function() { this.dataset = new Dataset({ name: 'test', source: this.source = jasmine.createSpy('source'), cache: false, }); this.source.and.callFake(fakeGetWithSyncResultsAndExtraParams); this.dataset.update('woah'); expect(this.source.calls.count()).toBe(1); expect(this.dataset.getRoot()).toContainText('one'); expect(this.dataset.getRoot()).toContainText('two'); expect(this.dataset.getRoot()).toContainText('three'); this.dataset.clear(); this.dataset.update('woah'); expect(this.source.calls.count()).toBe(2); expect(this.dataset.getRoot()).toContainText('one'); expect(this.dataset.getRoot()).toContainText('two'); expect(this.dataset.getRoot()).toContainText('three'); }); it('should reuse render function extra params for subsequent identical queries', function() { var spy = spyOn(this.dataset, '_render'); this.source.and.callFake(fakeGetWithSyncResultsAndExtraParams); this.dataset.update('woah'); expect(this.source.calls.count()).toBe(1); expect(spy.calls.argsFor(0)).toEqual([ 'woah', [ {value: 'one', raw: {value: 'one'}}, {value: 'two', raw: {value: 'two'}}, {value: 'three', raw: {value: 'three'}} ], 42, true, false]); this.dataset.clear(); this.dataset.update('woah'); expect(this.source.calls.count()).toBe(1); expect(spy.calls.argsFor(1)).toEqual([ 'woah', [ {value: 'one', raw: {value: 'one'}}, {value: 'two', raw: {value: 'two'}}, {value: 'three', raw: {value: 'three'}} ], 42, true, false]); }); it('should not retrieved cached results for subsequent different queries', function() { this.source.and.callFake(fakeGetWithSyncResultsAndExtraParams); this.dataset.update('woah'); expect(this.source.calls.count()).toBe(1); this.dataset.clear(); this.dataset.update('woah 2'); expect(this.source.calls.count()).toBe(2); }); it('should wait before calling the source if debounce is provided', function(done) { var that = this; this.dataset = new Dataset({ source: this.source, debounce: 250 }); this.source.and.callFake(fakeGetWithSyncResultsAndExtraParams); this.dataset.update('woah'); expect(this.source.calls.count()).toBe(0); this.dataset.update('woah 2'); expect(this.source.calls.count()).toBe(0); setTimeout(function() { expect(that.source.calls.count()).toBe(1); done(); }, 500); }); it('should not call the source if update was canceled', function(done) { var that = this; this.dataset = new Dataset({ source: this.source, debounce: 250 }); this.source.and.callFake(fakeGetWithSyncResultsAndExtraParams); this.dataset.update('woah'); expect(this.source.calls.count()).toBe(0); this.dataset.update('woah 2'); expect(this.source.calls.count()).toBe(0); this.dataset.clear(); expect(this.source.calls.count()).toBe(0); setTimeout(function() { expect(that.source.calls.count()).toBe(0); done(); }, 500); }); }); describe('#cacheSuggestions', function() { it('should assign cachedQuery, cachedSuggestions and cachedRenderArgs properties', function() { this.dataset.cacheSuggestions('woah', ['one', 'two'], 42); expect(this.dataset.cachedQuery).toEqual('woah'); expect(this.dataset.cachedSuggestions).toEqual(['one', 'two']); expect(this.dataset.cachedRenderExtraArgs).toEqual(42); }); }); describe('#clearCachedSuggestions', function() { it('should delete cachedQuery and cachedSuggestions properties', function() { this.dataset.cachedQuery = 'one'; this.dataset.cachedSuggestions = ['one', 'two']; this.dataset.cachedRenderExtraArgs = 42; this.dataset.clearCachedSuggestions(); expect(this.dataset.cachedQuery).toBeUndefined(); expect(this.dataset.cachedSuggestions).toBeUndefined(); expect(this.dataset.cachedRenderExtraArgs).toBeUndefined(); }); }); describe('#clear', function() { it('should clear suggestions', function() { this.source.and.callFake(fakeGetWithSyncResults); this.dataset.update('woah'); this.dataset.clear(); expect(this.dataset.getRoot()).toBeEmpty(); }); it('should cancel pending updates', function() { var spy = spyOn(this.dataset, 'cancel'); this.source.and.callFake(fakeGetWithSyncResults); this.dataset.update('woah'); expect(this.dataset.canceled).toBe(false); this.dataset.clear(); expect(spy).toHaveBeenCalled(); }); it('should not throw if called right after destroy()', function() { this.source.and.callFake(fakeGetWithSyncResults); this.dataset.update('woah'); this.dataset.destroy(); this.dataset.clear(); expect(this.dataset.getRoot()).toBeNull(); }); }); describe('#isEmpty', function() { it('should return true when empty', function() { expect(this.dataset.isEmpty()).toBe(true); }); it('should return false when not empty', function() { this.source.and.callFake(fakeGetWithSyncResults); this.dataset.update('woah'); expect(this.dataset.isEmpty()).toBe(false); }); }); describe('#destroy', function() { it('should null out the reference to the dataset element', function() { this.dataset.destroy(); expect(this.dataset.$el).toBeNull(); }); it('should clear suggestion cache', function() { var spy = spyOn(this.dataset, 'clearCachedSuggestions'); this.dataset.destroy(); expect(spy).toHaveBeenCalled(); }); }); // helper functions // ---------------- function fakeGetWithSyncResults(query, cb) { cb([ {value: 'one', raw: {value: 'one'}}, {value: 'two', raw: {value: 'two'}}, {value: 'three', raw: {value: 'three'}} ]); } function fakeGetForDisplayFn(query, cb) { cb([{display: '4'}, {display: '5'}, {display: '6'}]); } function fakeGetWithSyncNonArrayResults(query, cb) { cb({}); } function fakeGetWithSyncEmptyResults(query, cb) { cb(); } function fakeGetWithSyncEmptyResultsAndExtraParams(query, cb) { cb([], 42, true, false); } function fakeGetWithSyncResultsAndExtraParams(query, cb) { cb([ {value: 'one', raw: {value: 'one'}}, {value: 'two', raw: {value: 'two'}}, {value: 'three', raw: {value: 'three'}} ], 42, true, false); } function fakeGetWithAsyncResults(query, cb) { setTimeout(function() { cb([ {value: 'four', raw: {value: 'four'}}, {value: 'five', raw: {value: 'five'}} ]); }, 0); } }); ================================================ FILE: js/autocomplete.js/test/unit/dropdown_spec.js ================================================ 'use strict'; /* eslint-env mocha, jasmine */ describe('Dropdown', function() { require('../../src/common/dom.js').element = require('jquery'); require('../../src/jquery/plugin.js'); var $ = require('jquery'); require('jasmine-jquery'); var Dropdown = require('../../src/autocomplete/dropdown.js'); var fixtures = require('../fixtures.js'); var mocks = require('../helpers/mocks.js'); Dropdown.Dataset = mocks(Dropdown.Dataset); beforeEach(function() { var $fixture; setFixtures(fixtures.html.menu); $fixture = $('#jasmine-fixtures'); this.$menu = $fixture.find('.aa-dropdown-menu'); this.$menu.html(fixtures.html.dataset); this.view = new Dropdown({menu: this.$menu, datasets: [{}]}); this.dataset = this.view.datasets[0]; }); it('should throw an error if menu and/or datasets is missing', function() { expect(noMenu).toThrow(); expect(noDatasets).toThrow(); function noMenu() { new Dropdown({menu: '.menu'}); } function noDatasets() { new Dropdown({datasets: true}); } }); describe('when click event is triggered on a suggestion', function() { it('should trigger suggestionClicked', function() { var spy; this.view.onSync('suggestionClicked', spy = jasmine.createSpy()); this.$menu.find('.aa-suggestion').first().click(); expect(spy).toHaveBeenCalled(); }); }); describe('when instantiated with a custom header or footer', function() { beforeEach(function() { this.view.destroy(); this.view = new Dropdown({ menu: this.$menu, datasets: [{}], templates: { header: function() { return '

Header

'; }, footer: '' } }); }); it('should include the header', function() { expect(this.$menu.find('h2.header').length).toEqual(1); }); it('should include the footer', function() { expect(this.$menu.find('h2.footer').length).toEqual(1); }); }); describe('when instantiated with a custom empty template', function() { beforeEach(function() { this.view.destroy(); this.view = new Dropdown({ menu: this.$menu, datasets: [{}], templates: { empty: '

this is empty

' }, minLength: 4 }); }); it('should include the empty', function() { expect(this.$menu.find('.aa-empty').length).toEqual(1); expect(this.$menu.find('.aa-empty').children().length).toEqual(0); expect(this.$menu.find('.aa-empty').css('display')).toEqual('none'); }); it('should not hide the dropdown if empty', function() { this.view.datasets[0].isEmpty.and.returnValue(true); this.view.open(); this.view._onRendered('rendered', 'a query'); expect(this.$menu.find('.aa-empty').length).toEqual(1); expect(this.$menu.find('.aa-empty').children().length).toEqual(1); expect(this.$menu.find('.aa-empty').find('h3.empty').length).toEqual(1); expect(this.$menu.find('.aa-empty').css('display')).not.toEqual('none'); }); it('should trigger empty', function() { var spy; this.view.datasets[0].isEmpty.and.returnValue(true); this.view.onSync('empty', spy = jasmine.createSpy()); this.view.open(); this.view._onRendered('rendered', 'a query'); expect(spy).toHaveBeenCalled(); }); it('should not trigger empty if minLength is no satisfied', function() { var spy; this.view.datasets[0].isEmpty.and.returnValue(true); this.view.onSync('empty', spy = jasmine.createSpy()); this.view.open(); this.view._onRendered('rendered', 'te'); expect(spy).not.toHaveBeenCalled(); expect(this.$menu.find('.aa-empty').css('display')).toEqual('none'); }); }); describe('when mouseenter is triggered on a suggestion', function() { it('should remove pre-existing cursor', function(done) { var $first; var $last; $first = this.$menu.find('.aa-suggestion').first(); $last = this.$menu.find('.aa-suggestion').last(); $first.addClass('aa-cursor'); $last.mouseenter(); // see implementation on why we need this setTimeout setTimeout(function() { expect($first).not.toHaveClass('aa-cursor'); expect($last).toHaveClass('aa-cursor'); done(); }, 0); }); it('should set the cursor', function(done) { var $suggestion; $suggestion = this.$menu.find('.aa-suggestion').first(); $suggestion.mouseenter(); // see implementation on why we need this setTimeout setTimeout(function() { expect($suggestion).toHaveClass('aa-cursor'); done(); }, 0); }); it('should trigger cursorMoved', function(done) { var spy; var $suggestion; this.view.onSync('cursorMoved', spy = jasmine.createSpy()); $suggestion = this.$menu.find('.aa-suggestion').first(); $suggestion.mouseenter(); setTimeout(function() { expect(spy).toHaveBeenCalled(); done(); }, 0); }); }); describe('when mouseleave is triggered on a suggestion', function() { it('should remove the cursor', function() { var $suggestion; $suggestion = this.$menu.find('.aa-suggestion').first(); $suggestion.mouseenter().mouseleave(); expect($suggestion).not.toHaveClass('aa-cursor'); }); }); describe('when rendered is triggered on a dataset', function() { it('should hide the dropdown if empty', function() { this.dataset.isEmpty.and.returnValue(true); this.view.open(); this.view._show(); this.dataset.trigger('rendered', 'the query'); expect(this.$menu).not.toBeVisible(); }); it('should show the dropdown if not empty', function() { this.dataset.isEmpty.and.returnValue(false); this.view.open(); this.dataset.trigger('rendered', 'the query'); expect(this.$menu).toBeVisible(); }); it('should trigger datasetRendered', function() { var spy; this.view.onSync('datasetRendered', spy = jasmine.createSpy()); this.dataset.trigger('rendered'); expect(spy).toHaveBeenCalled(); }); }); describe('#open', function() { it('should display the menu if not empty', function() { this.view.isOpen = true; this.view.close(); expect(this.$menu).not.toBeVisible(); this.view.isEmpty = false; this.view.open(); expect(this.$menu).toBeVisible(); }); it('should not display the menu if empty', function() { this.view.isOpen = true; this.view.close(); expect(this.$menu).not.toBeVisible(); this.view.isEmpty = true; this.view.open(); expect(this.$menu).not.toBeVisible(); }); it('should trigger opened', function() { var spy; this.view.onSync('opened', spy = jasmine.createSpy()); this.view.close(); this.view.open(); expect(spy).toHaveBeenCalled(); }); it('should trigger shown', function() { var spy; this.view.onSync('shown', spy = jasmine.createSpy()); this.view.close(); this.view.isEmpty = false; this.view.open(); expect(spy).toHaveBeenCalled(); }); it('should trigger redrawn', function() { var spy; this.view.onSync('redrawn', spy = jasmine.createSpy()); this.view.close(); this.view.isEmpty = false; this.view.appendTo = 'body'; this.view.open(); expect(spy).toHaveBeenCalled(); }); }); describe('#close', function() { it('should hide the menu', function() { this.view.open(); this.view.close(); expect(this.$menu).not.toBeVisible(); }); it('should trigger closed', function() { var spy; this.view.onSync('closed', spy = jasmine.createSpy()); this.view.open(); this.view.close(); expect(spy).toHaveBeenCalled(); }); }); describe('#setLanguageDirection', function() { it('should update css for given language direction', function() { // TODO: eh, the toHaveCss matcher doesn't seem to work very well /* this.view.setLanguageDirection('rtl'); expect(this.$menu).toHaveCss({ left: 'auto', right: '0px' }); this.view.setLanguageDirection('ltr'); expect(this.$menu).toHaveCss({ left: '0px', right: 'auto' }); */ }); }); describe('#moveCursorUp', function() { beforeEach(function() { this.view.open(); }); it('should move the cursor up', function() { var $first; var $second; $first = this.view._getSuggestions().eq(0); $second = this.view._getSuggestions().eq(1); this.view._setCursor($second); this.view.moveCursorUp(); expect(this.view._getCursor()).toContainText($first.text()); }); it('should move cursor to bottom if cursor is not present', function() { var $bottom; $bottom = this.view._getSuggestions().eq(-1); this.view.moveCursorUp(); expect(this.view._getCursor()).toContainText($bottom.text()); }); it('should remove cursor if already at top', function() { var $first; $first = this.view._getSuggestions().eq(0); this.view._setCursor($first); this.view.moveCursorUp(); expect(this.view._getCursor().length).toBe(0); }); }); describe('#moveCursorDown', function() { beforeEach(function() { this.view.open(); }); it('should move the cursor down', function() { var $first; var $second; $first = this.view._getSuggestions().eq(0); $second = this.view._getSuggestions().eq(1); this.view._setCursor($first); this.view.moveCursorDown(); expect(this.view._getCursor()).toContainText($second.text()); }); it('should move cursor to top if cursor is not present', function() { var $first; $first = this.view._getSuggestions().eq(0); this.view.moveCursorDown(); expect(this.view._getCursor()).toContainText($first.text()); }); it('should remove cursor if already at bottom', function() { var $bottom; $bottom = this.view._getSuggestions().eq(-1); this.view._setCursor($bottom); this.view.moveCursorDown(); expect(this.view._getCursor().length).toBe(0); }); }); describe('#getDatumForSuggestion', function() { it('should extract the datum from the suggestion element', function() { var $suggestion; var datum; $suggestion = $('
').data({aaValue: 'one', aaDatum: JSON.stringify('two')}); datum = this.view.getDatumForSuggestion($suggestion); expect(datum).toEqual({value: 'one', raw: 'two', datasetName: undefined}); }); it('should return null if no element is given', function() { expect(this.view.getDatumForSuggestion($('notreal'))).toBeNull(); }); }); describe('#getDatumForCursor', function() { it('should return the datum for the cursor', function() { var $first; $first = this.view._getSuggestions().eq(0); $first.data({aaValue: 'one', aaDatum: JSON.stringify('two')}); this.view._setCursor($first); expect(this.view.getDatumForCursor()) .toEqual({value: 'one', raw: 'two', datasetName: undefined}); }); }); describe('#getDatumForTopSuggestion', function() { it('should return the datum for top suggestion', function() { var $first; $first = this.view._getSuggestions().eq(0); $first.data({aaValue: 'one', aaDatum: JSON.stringify('two')}); expect(this.view.getDatumForTopSuggestion()) .toEqual({value: 'one', raw: 'two', datasetName: undefined}); }); }); describe('#update', function() { it('should invoke update on each dataset', function() { this.view.update(); expect(this.dataset.update).toHaveBeenCalled(); }); }); describe('#empty', function() { it('should invoke clear on each dataset', function() { this.view.empty(); expect(this.dataset.clear).toHaveBeenCalled(); }); }); describe('#isVisible', function() { it('should return true if open and not empty', function() { this.view.isOpen = true; this.view.isEmpty = false; expect(this.view.isVisible()).toBe(true); this.view.isOpen = false; this.view.isEmpty = false; expect(this.view.isVisible()).toBe(false); this.view.isOpen = true; this.view.isEmpty = true; expect(this.view.isVisible()).toBe(false); this.view.isOpen = false; this.view.isEmpty = false; expect(this.view.isVisible()).toBe(false); }); }); describe('#destroy', function() { it('should remove event handlers', function() { var $menu = this.view.$menu; spyOn($menu, 'off'); this.view.destroy(); expect($menu.off).toHaveBeenCalledWith('.aa'); }); it('should destroy its datasets', function() { this.view.destroy(); expect(this.dataset.destroy).toHaveBeenCalled(); }); it('should null out its reference to the menu element', function() { this.view.destroy(); expect(this.view.$menu).toBeNull(); }); }); }); ================================================ FILE: js/autocomplete.js/test/unit/event_emitter_spec.js ================================================ 'use strict'; /* eslint-env mocha, jasmine */ describe('EventEmitter', function() { require('../../src/common/dom.js').element = require('jquery'); require('../../src/jquery/plugin.js'); var EventEmitter = require('../../src/autocomplete/event_emitter.js'); var _ = require('../../src/common/utils.js'); var waitsForAndRuns = require('../helpers/waits_for.js'); beforeEach(function() { this.spy = jasmine.createSpy(); this.target = _.mixin({}, EventEmitter); }); it('methods should be chainable', function() { expect(this.target.onSync()).toEqual(this.target); expect(this.target.onAsync()).toEqual(this.target); expect(this.target.off()).toEqual(this.target); expect(this.target.trigger()).toEqual(this.target); }); it('#on should take the context a callback should be called in', function(done) { var context = {val: 3}; var cbContext; this.target.onSync('xevent', setCbContext, context).trigger('xevent'); waitsForAndRuns(assertCbContext, done, 100); function setCbContext() { cbContext = this; } function assertCbContext() { return cbContext === context; } }); it('#onAsync callbacks should be invoked asynchronously', function(done) { this.target.onAsync('event', this.spy).trigger('event'); expect(this.spy.calls.count()).toBe(0); waitsForAndRuns(assertCallCount(this.spy, 1), done, 100); }); it('#onSync callbacks should be invoked synchronously', function() { this.target.onSync('event', this.spy).trigger('event'); expect(this.spy.calls.count()).toBe(1); }); it('#off should remove callbacks', function(done) { this.target .onSync('event1 event2', this.spy) .onAsync('event1 event2', this.spy) .off('event1 event2') .trigger('event1 event2'); setTimeout(assertCallCount(this.spy, 0, done), 100); }); it('methods should accept multiple event types', function(done) { this.target .onSync('event1 event2', this.spy) .onAsync('event1 event2', this.spy) .trigger('event1 event2'); expect(this.spy.calls.count()).toBe(2); setTimeout(assertCallCount(this.spy, 4, done), 100); }); it('the event type should be passed to the callback', function(done) { this.target .onSync('sync', this.spy) .onAsync('async', this.spy) .trigger('sync async'); var that = this; waitsForAndRuns(assertArgs(this.spy, 0, ['sync']), function() { waitsForAndRuns(assertArgs(that.spy, 1, ['async']), done, 100); }, 100); }); it('arbitrary args should be passed to the callback', function(done) { this.target .onSync('event', this.spy) .onAsync('event', this.spy) .trigger('event', 1, 2); var that = this; waitsForAndRuns(assertArgs(this.spy, 0, ['event', 1, 2]), function() { waitsForAndRuns(assertArgs(that.spy, 1, ['event', 1, 2]), done, 100); }, 100); }); it('callback execution should be cancellable', function(done) { var cancelSpy = jasmine.createSpy().and.callFake(cancel); this.target .onSync('one', cancelSpy) .onSync('one', this.spy) .onAsync('two', cancelSpy) .onAsync('two', this.spy) .onSync('three', cancelSpy) .onAsync('three', this.spy) .trigger('one two three'); var that = this; setTimeout(assertCallCount(cancelSpy, 3, function() { setTimeout(assertCallCount(that.spy, 0, done), 100); }), 100); function cancel() { return false; } }); function assertCallCount(spy, expected, done) { return function() { expect(spy.calls.count()).toBe(expected); done && done(); }; } function assertArgs(spy, call, expected) { return function() { var actual = spy.calls.argsFor(call); return expect(actual).toEqual(expected); }; } }); ================================================ FILE: js/autocomplete.js/test/unit/hits_spec.js ================================================ 'use strict'; /* eslint-env mocha, jasmine */ describe('hits', function () { var hitsSource = require('../../src/sources/hits.js'); var version = require('../../version.js'); var client = { _ua: 'javascript wrong agent', search: function search(requests) { return window.Promise.resolve({ results: requests.map(function (request) { return { index: request.indexName, hits: [ {value: 'Q1-' + request.indexName}, {value: 'Q2-' + request.indexName}, {value: 'Q3-' + request.indexName} ] }; }) }); } }; it('returns results from one index', function () { var suggestions = []; var f = hitsSource( { as: client, indexName: 'products' }, {hitsPerPage: 3} ); // wait only on one promise, this asserts that our "promise.resolve" trick works f('q', function cb1(hits) { suggestions = suggestions.concat(hits); }); // force the rest of our test to be more than a microtask behind return new Promise(function (res) { setTimeout(res, 0); }).then(function () { expect(suggestions.length).toEqual(3); expect(suggestions[0].value).toEqual('Q1-products'); expect(suggestions[1].value).toEqual('Q2-products'); expect(suggestions[2].value).toEqual('Q3-products'); }); }); it('returns results from multiple indices', function () { var suggestions1 = []; var suggestions2 = []; var f1 = hitsSource( { as: client, indexName: 'products' }, {hitsPerPage: 3} ); var f2 = hitsSource( { as: client, indexName: 'other' }, {hitsPerPage: 3} ); f1('q', function cb1(hits) { suggestions1 = suggestions1.concat(hits); }); f2('q', function cb2(hits) { suggestions2 = suggestions2.concat(hits); }); // force the rest of our test to be more than a microtask behind return new Promise(function (res) { setTimeout(res, 0); }).then(function () { expect(suggestions1.length).toEqual(3); expect(suggestions1[0].value).toEqual('Q1-products'); expect(suggestions1[1].value).toEqual('Q2-products'); expect(suggestions1[2].value).toEqual('Q3-products'); expect(suggestions2.length).toEqual(3); expect(suggestions2[0].value).toEqual('Q1-other'); expect(suggestions2[1].value).toEqual('Q2-other'); expect(suggestions2[2].value).toEqual('Q3-other'); }); }); it('calls client.search only once', function () { var suggestions1 = []; var suggestions2 = []; var searchSpy = spyOn(client, 'search').and.callThrough(); var f1 = hitsSource( { as: client, indexName: 'products' }, {hitsPerPage: 3} ); var f2 = hitsSource( { as: client, indexName: 'other' }, {hitsPerPage: 3} ); // wait only on one promise, this asserts that our "promise.resolve" trick works f1('q', function cb1(hits) { suggestions1 = suggestions1.concat(hits); }); f2('q', function cb2(hits) { suggestions2 = suggestions2.concat(hits); }); // force the rest of our test to be more than a microtask behind return new Promise(function (res) { setTimeout(res, 0); }).then(function () { expect(searchSpy).toHaveBeenCalledTimes(1); }); }); it('does not augment the _ua if not JS client v3', function () { expect(client._ua).toEqual('javascript wrong agent'); hitsSource( { as: client, indexName: 'products' }, {hitsPerPage: 3} ); expect(client._ua).toEqual('javascript wrong agent'); }); it('augments the _ua once', function () { client._ua = 'Algolia for JavaScript (3.35.0)'; expect(client._ua).toEqual('Algolia for JavaScript (3.35.0)'); hitsSource( { as: client, indexName: 'products' }, {hitsPerPage: 3} ); expect(client._ua).toEqual( 'Algolia for JavaScript (3.35.0); autocomplete.js ' + version ); hitsSource( { as: client, indexName: 'something' }, {hitsPerPage: 70} ); expect(client._ua).toEqual( 'Algolia for JavaScript (3.35.0); autocomplete.js ' + version ); }); }); ================================================ FILE: js/autocomplete.js/test/unit/input_spec.js ================================================ 'use strict'; /* eslint-env mocha, jasmine */ describe('Input', function() { var $ = require('jquery'); require('jasmine-jquery'); require('../../src/common/dom.js').element = $; require('../../src/jquery/plugin.js'); var Input = require('../../src/autocomplete/input.js'); var _ = require('../../src/common/utils.js'); var fixtures = require('../fixtures.js'); var waitsForAndRuns = require('../helpers/waits_for.js'); var KEYS; KEYS = { enter: 13, esc: 27, tab: 9, left: 37, right: 39, up: 38, down: 40, normal: 65 // "A" key }; beforeEach(function() { var $fixture; setFixtures(fixtures.html.input + fixtures.html.hint); $fixture = $('#jasmine-fixtures'); this.$input = $fixture.find('.aa-input'); this.$hint = $fixture.find('.aa-hint'); this.view = new Input({input: this.$input, hint: this.$hint}); }); it('should throw an error if no hint and/or input is provided', function() { expect(noInput).toThrow(); function noInput() { new Input({hint: '.hint'}); } }); describe('when the blur DOM event is triggered', function() { it('should reset the input value', function() { this.view.setQuery('wine'); this.view.setInputValue('cheese', true); this.$input.blur(); expect(this.$input.val()).toBe('wine'); }); it('should trigger blurred', function() { var spy; this.view.onSync('blurred', spy = jasmine.createSpy()); this.$input.blur(); expect(spy).toHaveBeenCalled(); }); }); describe('when the focus DOM event is triggered', function() { it('should trigger focused', function() { var spy; this.view.onSync('focused', spy = jasmine.createSpy()); this.$input.focus(); expect(spy).toHaveBeenCalled(); }); }); describe('when the keydown DOM event is triggered by tab', function() { it('should trigger tabKeyed if no modifiers were pressed', function() { var spy; this.view.onSync('tabKeyed', spy = jasmine.createSpy()); simulateKeyEvent(this.$input, 'keydown', KEYS.tab); expect(spy).toHaveBeenCalled(); }); it('should not trigger tabKeyed if modifiers were pressed', function() { var spy; this.view.onSync('tabKeyed', spy = jasmine.createSpy()); simulateKeyEvent(this.$input, 'keydown', KEYS.tab, true); expect(spy).not.toHaveBeenCalled(); }); it('should prevent default behavior if there is a hint', function() { var $e; this.view.setHint('good'); this.view.setInputValue('goo'); $e = simulateKeyEvent(this.$input, 'keydown', KEYS.tab); expect($e.preventDefault).toHaveBeenCalled(); }); }); describe('when the keydown DOM event is triggered by esc', function() { it('should trigger escKeyed', function() { var spy; this.view.onSync('escKeyed', spy = jasmine.createSpy()); simulateKeyEvent(this.$input, 'keydown', KEYS.esc); expect(spy).toHaveBeenCalled(); }); }); describe('when the keydown DOM event is triggered by left', function() { it('should trigger leftKeyed', function() { var spy; this.view.onSync('leftKeyed', spy = jasmine.createSpy()); simulateKeyEvent(this.$input, 'keydown', KEYS.left); expect(spy).toHaveBeenCalled(); }); }); describe('when the keydown DOM event is triggered by right', function() { it('should trigger rightKeyed', function() { var spy; this.view.onSync('rightKeyed', spy = jasmine.createSpy()); simulateKeyEvent(this.$input, 'keydown', KEYS.right); expect(spy).toHaveBeenCalled(); }); }); describe('when the keydown DOM event is triggered by enter', function() { it('should trigger enterKeyed', function() { var spy; this.view.onSync('enterKeyed', spy = jasmine.createSpy()); simulateKeyEvent(this.$input, 'keydown', KEYS.enter); expect(spy).toHaveBeenCalled(); }); }); describe('when the keydown DOM event is triggered by up', function() { it('should trigger upKeyed', function() { var spy; this.view.onSync('upKeyed', spy = jasmine.createSpy()); simulateKeyEvent(this.$input, 'keydown', KEYS.up); expect(spy).toHaveBeenCalled(); }); it('should prevent default if no modifers were pressed', function() { var $e = simulateKeyEvent(this.$input, 'keydown', KEYS.up); expect($e.preventDefault).toHaveBeenCalled(); }); it('should not prevent default if modifers were pressed', function() { var $e = simulateKeyEvent(this.$input, 'keydown', KEYS.up, true); expect($e.preventDefault).not.toHaveBeenCalled(); }); }); describe('when the keydown DOM event is triggered by down', function() { it('should trigger downKeyed', function() { var spy; this.view.onSync('downKeyed', spy = jasmine.createSpy()); simulateKeyEvent(this.$input, 'keydown', KEYS.down); expect(spy).toHaveBeenCalled(); }); it('should prevent default if no modifers were pressed', function() { var $e = simulateKeyEvent(this.$input, 'keydown', KEYS.down); expect($e.preventDefault).toHaveBeenCalled(); }); it('should not prevent default if modifers were pressed', function() { var $e = simulateKeyEvent(this.$input, 'keydown', KEYS.down, true); expect($e.preventDefault).not.toHaveBeenCalled(); }); }); // NOTE: have to treat these as async because the ie polyfill acts // in a async manner describe('when the input DOM event is triggered', function() { it('should update query', function(done) { this.view.setQuery('wine'); this.view.setInputValue('cheese', true); simulateInputEvent(this.$input); var that = this; waitsForAndRuns(function() { return that.view.getQuery() === 'cheese'; }, done, 100); }); it('should trigger queryChanged if the query changed', function() { var spy; this.view.setQuery('wine'); this.view.setInputValue('cheese', true); this.view.onSync('queryChanged', spy = jasmine.createSpy()); simulateInputEvent(this.$input); expect(spy).toHaveBeenCalled(); }); it('should trigger whitespaceChagned if whitespace changed', function() { var spy; this.view.setQuery('wine bar'); this.view.setInputValue('wine bar', true); this.view.onSync('whitespaceChanged', spy = jasmine.createSpy()); simulateInputEvent(this.$input); expect(spy).toHaveBeenCalled(); }); }); describe('#focus', function() { it('should focus the input', function() { this.$input.blur(); this.view.focus(); expect(this.$input).toBeFocused(); }); }); describe('#blur', function() { it('should blur the input', function() { this.$input.focus(); this.view.blur(); expect(this.$input).not.toBeFocused(); }); }); describe('#getQuery/#setQuery', function() { it('should act as getter/setter to the query property', function() { this.view.setQuery('mouse'); expect(this.view.getQuery()).toBe('mouse'); }); }); describe('#getInputValue', function() { it('should act as getter to the input value', function() { this.$input.val('cheese'); expect(this.view.getInputValue()).toBe('cheese'); }); }); describe('#setInputValue', function() { it('should act as setter to the input value', function() { this.view.setInputValue('cheese'); expect(this.view.getInputValue()).toBe('cheese'); }); it('should not set the current query if null', function() { this.view.setQuery('cheese'); this.view.setInputValue(null); expect(this.view.getInputValue()).toBe(''); }); it('should set the current query if undefined', function() { this.view.setQuery('cheese'); this.view.setInputValue(undefined); expect(this.view.getInputValue()).toBe('cheese'); }); it('should trigger {query|whitespace}Changed when applicable', function() { var spy1; var spy2; this.view.onSync('queryChanged', spy1 = jasmine.createSpy()); this.view.onSync('whitespaceChanged', spy2 = jasmine.createSpy()); this.view.setInputValue('cheese head'); expect(spy1).toHaveBeenCalled(); expect(spy2).not.toHaveBeenCalled(); this.view.setInputValue('cheese head'); expect(spy1.calls.count()).toBe(1); expect(spy2).toHaveBeenCalled(); }); }); describe('#setActiveDescendant', function() { it('should set the aria-activedescendant attribute', function() { this.view.setActiveDescendant('abc'); expect(this.$input.attr('aria-activedescendant')).toBe('abc'); }); }); describe('#removeActiveDescendant', function() { it('should remove the aria-activedescendant attribute', function() { this.view.setActiveDescendant('foo'); expect(this.$input.attr('aria-activedescendant')).toBe('foo'); this.view.removeActiveDescendant('bar'); expect(this.$input.attr('aria-activedescendant')).toBeUndefined(); }); }); describe('#getHint/#setHint', function() { it('should act as getter/setter to value of hint', function() { this.view.setHint('mountain'); expect(this.view.getHint()).toBe('mountain'); }); }); describe('#resetInputValue', function() { it('should reset input value to last query', function() { this.view.setQuery('cheese'); this.view.setInputValue('wine', true); this.view.resetInputValue(); expect(this.view.getInputValue()).toBe('cheese'); }); }); describe('#clearHint', function() { it('should set the hint value to the empty string', function() { this.view.setHint('cheese'); this.view.clearHint(); expect(this.view.getHint()).toBe(''); }); }); describe('#clearHintIfInvalid', function() { it('should clear hint if input value is empty string', function() { this.view.setInputValue('', true); this.view.setHint('cheese'); this.view.clearHintIfInvalid(); expect(this.view.getHint()).toBe(''); }); it('should clear hint if input value is not prefix of input', function() { this.view.setInputValue('milk', true); this.view.setHint('cheese'); this.view.clearHintIfInvalid(); expect(this.view.getHint()).toBe(''); }); it('should clear hint if overflow exists', function() { spyOn(this.view, 'hasOverflow').and.returnValue(true); this.view.setInputValue('che', true); this.view.setHint('cheese'); this.view.clearHintIfInvalid(); expect(this.view.getHint()).toBe(''); }); it('should not clear hint if input value is prefix of input', function() { this.view.setInputValue('che', true); this.view.setHint('cheese'); this.view.clearHintIfInvalid(); expect(this.view.getHint()).toBe('cheese'); }); }); describe('#getLanguageDirection', function() { it('should return the language direction of the input', function() { this.$input.css('direction', 'ltr'); expect(this.view.getLanguageDirection()).toBe('ltr'); this.$input.css('direction', 'rtl'); expect(this.view.getLanguageDirection()).toBe('rtl'); }); }); describe('#hasOverflow', function() { it('should return true if the input has overflow text', function() { var longStr = new Array(1000).join('a'); this.view.setInputValue(longStr); expect(this.view.hasOverflow()).toBe(true); }); it('should return false if the input has no overflow text', function() { var shortStr = 'aah'; this.view.setInputValue(shortStr); expect(this.view.hasOverflow()).toBe(false); }); }); describe('#isCursorAtEnd', function() { it('should return true if the text cursor is at the end', function() { this.view.setInputValue('boo'); setCursorPosition(this.$input, 3); expect(this.view.isCursorAtEnd()).toBe(true); }); it('should return false if the text cursor is not at the end', function() { this.view.setInputValue('boo'); setCursorPosition(this.$input, 1); expect(this.view.isCursorAtEnd()).toBe(false); }); }); describe('#destroy', function() { it('should remove event handlers', function() { var $input; var $hint; $hint = this.view.$hint; $input = this.view.$input; spyOn($hint, 'off'); spyOn($input, 'off'); this.view.destroy(); expect($hint.off).toHaveBeenCalledWith('.aa'); expect($input.off).toHaveBeenCalledWith('.aa'); }); it('should null out its reference to DOM elements', function() { this.view.destroy(); expect(this.view.$hint).toBeNull(); expect(this.view.$input).toBeNull(); expect(this.view.$overflowHelper).toBeNull(); }); }); // helper functions // ---------------- function simulateInputEvent($node) { var $e; var type; type = _.isMsie() ? 'keypress' : 'input'; $e = $.Event(type); $node.trigger($e); } function simulateKeyEvent($node, type, key, withModifier) { var $e; $e = $.Event(type, { keyCode: key, altKey: !!withModifier, ctrlKey: !!withModifier, metaKey: !!withModifier, shiftKey: !!withModifier }); spyOn($e, 'preventDefault'); $node.trigger($e); return $e; } function setCursorPosition($input, pos) { var input = $input[0]; var range; if (input.setSelectionRange) { input.focus(); input.setSelectionRange(pos, pos); } else if (input.createTextRange) { range = input.createTextRange(); range.collapse(true); range.moveEnd('character', pos); range.moveStart('character', pos); range.select(); } } }); ================================================ FILE: js/autocomplete.js/test/unit/jquery_spec.js ================================================ 'use strict'; /* eslint-env mocha, jasmine */ describe('Typeahead', function() { var $ = require('jquery'); require('jasmine-jquery'); var fixtures = require('../fixtures.js'); var $autocomplete = require('../../src/jquery/plugin.js'); describe('when instantiated from jquery', function() { beforeEach(function() { this.$fixture = $(setFixtures(fixtures.html.textInput)); this.view = this.$fixture.find('input').autocomplete({}, [{ name: 'test', source: function(q, cb) { cb([{name: 'test'}]); }, templates: { suggestion: function(sugg) { return sugg.name; } } }]).data('aaAutocomplete'); }); it('should initialize', function() { expect(this.$fixture.find('.aa-dropdown-menu').length).toEqual(1); }); it('should open the dropdown', function() { this.$fixture.find('input').val('test'); expect(this.view.input.getInputValue()).toEqual('test'); $autocomplete.call($('input'), 'val', 'test'); $autocomplete.call($('input'), 'open'); $autocomplete.call($('input'), 'close'); }); }); }); ================================================ FILE: js/autocomplete.js/test/unit/parseAlgoliaClientVersion_spec.js ================================================ 'use strict'; /* eslint-env mocha, jasmine */ describe('parseAlgoliaClientVersion', function() { var parseAlgoliaClientVersion = require('../../src/common/parseAlgoliaClientVersion.js'); it('should return undefined for unknown user agents', function() { expect(parseAlgoliaClientVersion('random user agent 1.2.3')).toEqual( undefined ); }); it('should parse user agents with algoliasearch < 3.33.0 format', function() { expect( parseAlgoliaClientVersion('Algolia for vanilla JavaScript 3.1.0') ).toEqual(['3.', '1.', '0']); }); it('should parse user agents with algoliasearch >= 3.33.0 format', function() { expect(parseAlgoliaClientVersion('Algolia for JavaScript (3.5.0)')).toEqual([ '3.', '5.', '0' ]); }); }); ================================================ FILE: js/autocomplete.js/test/unit/popularIn_spec.js ================================================ 'use strict'; /* eslint-env mocha, jasmine */ describe('popularIn', function() { require('../../src/common/dom.js').element = require('jquery'); require('../../src/jquery/plugin.js'); var popularIn = require('../../src/sources/popularIn.js'); beforeEach(function() { }); function build(options) { var queries = { as: { _ua: 'javascript wrong agent', }, search: function(q, params, cb) { cb(false, { hits: [ { value: 'q1' }, { value: 'q2' }, { value: 'q3' } ] }); } }; var products = { as: { _ua: 'javascript wrong agent', }, search: function(q, params, cb) { cb(false, { facets: { category: { c1: 42, c2: 21, c3: 2 } } }) } }; var f = popularIn(queries, { hitsPerPage: 3 }, { source: 'value', index: products, facets: 'category', maxValuesPerFacet: 3 }, options); var suggestions = []; function cb(hits) { suggestions = suggestions.concat(hits); } f('q', cb); return suggestions; } it('should query 2 indices and build the combinatory', function() { var suggestions = build(); expect(suggestions.length).toEqual(5); expect(suggestions[0].value).toEqual('q1'); expect(suggestions[0].facet.value).toEqual('c1'); expect(suggestions[1].value).toEqual('q1'); expect(suggestions[1].facet.value).toEqual('c2'); expect(suggestions[2].value).toEqual('q1'); expect(suggestions[2].facet.value).toEqual('c3'); expect(suggestions[3].value).toEqual('q2'); expect(suggestions[3].facet).toBe(undefined); expect(suggestions[4].value).toEqual('q3'); expect(suggestions[4].facet).toBe(undefined); }); it('should include the all department entry', function() { var suggestions = build({includeAll: true}); expect(suggestions.length).toEqual(6); expect(suggestions[0].value).toEqual('q1'); expect(suggestions[0].facet.value).toEqual('All departments'); expect(suggestions[1].value).toEqual('q1'); expect(suggestions[1].facet.value).toEqual('c1'); expect(suggestions[2].value).toEqual('q1'); expect(suggestions[2].facet.value).toEqual('c2'); expect(suggestions[3].value).toEqual('q1'); expect(suggestions[3].facet.value).toEqual('c3'); expect(suggestions[4].value).toEqual('q2'); expect(suggestions[4].facet).toBe(undefined); expect(suggestions[5].value).toEqual('q3'); expect(suggestions[5].facet).toBe(undefined); }); it('should include the all department entry with a custom title', function() { var suggestions = build({includeAll: true, allTitle: 'ALL'}); expect(suggestions.length).toEqual(6); expect(suggestions[0].value).toEqual('q1'); expect(suggestions[0].facet.value).toEqual('ALL'); }); it('should not include the all department entry when no results', function() { var queries = { as: { _ua: 'Algolia for vanilla JavaScript 4.3.6' }, search: function(q, params, cb) { cb(false, { hits: [] }); } }; var products = { as: { _ua: 'javascript wrong agent', }, search: function(q, params, cb) { throw new Error('Never reached'); } }; var f = popularIn(queries, { hitsPerPage: 3 }, { source: 'value', index: products }, { includeAll: true }); var suggestions = []; function cb(hits) { suggestions = suggestions.concat(hits); } f('q', cb); expect(suggestions.length).toEqual(0); }); }); ================================================ FILE: js/autocomplete.js/test/unit/standalone_spec.js ================================================ 'use strict'; /* eslint-env mocha, jasmine */ describe('Typeahead', function() { var fixtures = require('../fixtures.js'); var autocomplete = require('../../src/standalone/index.js'); beforeEach(function() { this.$fixture = setFixtures(fixtures.html.textInput); this.ac = autocomplete('input', {}, { name: 'test', source: function(q, cb) { cb([{name: 'test'}]); }, templates: { suggestion: function(sugg) { return sugg.name; } } }); }); describe('when instantiated from standalone', function() { it('should initialize', function() { expect(this.$fixture.find('.aa-dropdown-menu').length).toEqual(1); }); it('has an .autocomplete property', function() { expect(this.ac.autocomplete).toBeDefined(); }); }); describe('when accessing autocomplete function', function() { it('should have an open, close, getVal, setVal and destroy methods', function() { var methodsToAssert = ['open', 'close', 'getVal', 'setVal', 'destroy', 'getWrapper']; for (var i = 0; i < methodsToAssert.length; i++) { expect(this.ac.autocomplete[methodsToAssert[i]]).toBeDefined(); expect(typeof this.ac.autocomplete[methodsToAssert[i]]).toEqual('function'); } }); describe('when executing the methods', function() { beforeEach(function() { this.$fixture = setFixtures(fixtures.html.textInput); this.typeaheadSpy = { input: { $input: {} }, open: sinon.stub().returns('hello'), close: sinon.stub().returns('hello'), getVal: sinon.stub().returns('hello'), setVal: sinon.stub().returns('hello'), destroy: sinon.stub().returns('hello'), getWrapper: sinon.stub().returns('hello') }; this.ac = autocomplete('input', {}, { name: 'test', source: function(q, cb) { cb([{name: 'test'}]); }, templates: { suggestion: function(sugg) { return sugg.name; } } }, this.typeaheadSpy); }); it('should proxy the method call on typeahead object', function() { expect(this.ac.autocomplete.open()).toEqual('hello'); expect(this.typeaheadSpy.open.calledOnce).toBe(true); expect(this.ac.autocomplete.close()).toEqual('hello'); expect(this.typeaheadSpy.close.calledOnce).toBe(true); expect(this.ac.autocomplete.getVal()).toEqual('hello'); expect(this.typeaheadSpy.getVal.calledOnce).toBe(true); expect(this.ac.autocomplete.setVal('Hey')).toEqual('hello'); expect(this.typeaheadSpy.setVal.withArgs('Hey').calledOnce).toBe(true); expect(this.ac.autocomplete.destroy()).toEqual('hello'); expect(this.typeaheadSpy.destroy.calledOnce).toBe(true); expect(this.ac.autocomplete.getWrapper()).toEqual('hello'); expect(this.typeaheadSpy.getWrapper.calledOnce).toBe(true); }); }); }); }); describe('noConflict()', function() { it('should restore the previous value of autocomplete', function() { window.autocomplete = 'test'; // Rerequire autocomplete delete require.cache[require.resolve('../../src/standalone/index.js')]; var autocomplete = require('../../src/standalone/index.js'); expect(window.autocomplete).toBe('test'); window.autocomplete = autocomplete; var aa = autocomplete.noConflict(); expect(aa).toBe(autocomplete); expect(window.autocomplete).toBe('test'); }); it('should delete window.autocomplete if it wasn\'t set', function() { if ('autocomplete' in window) { delete window.autocomplete; } // Rerequire autocomplete delete require.cache[require.resolve('../../src/standalone/index.js')]; var autocomplete = require('../../src/standalone/index.js'); expect('autocomplete' in window).toBe(false); window.autocomplete = autocomplete; var aa = autocomplete.noConflict(); expect(aa).toBe(autocomplete); expect('autocomplete' in window).toBe(false); }); }); ================================================ FILE: js/autocomplete.js/test/unit/typeahead_spec.js ================================================ 'use strict'; /* eslint-env mocha, jasmine */ describe('Typeahead', function() { require('../../src/common/dom.js').element = require('jquery'); require('../../src/jquery/plugin.js'); var $ = require('jquery'); require('jasmine-jquery'); var Typeahead = require('../../src/autocomplete/typeahead.js'); var fixtures = require('../fixtures.js'); var mocks = require('../helpers/mocks.js'); var waitsForAndRuns = require('../helpers/waits_for.js'); Typeahead.Dropdown = mocks(Typeahead.Dropdown); Typeahead.Input = mocks(Typeahead.Input); var testDatum; beforeEach(function() { var $fixture; var $input; setFixtures(fixtures.html.textInput); $fixture = $('#jasmine-fixtures'); this.$input = $fixture.find('input'); testDatum = fixtures.data.simple[0]; this.view = new Typeahead({ input: this.$input, hint: true, datasets: {} }); this.input = this.view.input; this.dropdown = this.view.dropdown; }); describe('appendTo', function() { it('should throw if used with hint', function(done) { expect(function() { return new Typeahead({ input: this.$input, hint: true, appendTo: 'body' }); }).toThrow(); done(); }); it('should be appended to the target of appendTo', function(done) { var node = document.createElement('div'); document.querySelector('body').appendChild(node); expect(node.children.length).toEqual(0); this.view.destroy(); this.view = new Typeahead({ input: this.$input, hint: false, appendTo: node }); expect(document.querySelectorAll('.algolia-autocomplete').length).toEqual(1); expect(node.children.length).toEqual(1); this.view.destroy(); done(); }); }); describe('when dropdown triggers suggestionClicked', function() { beforeEach(function() { this.dropdown.getDatumForSuggestion.and.returnValue(testDatum); }); it('should select the datum', function(done) { var spy; this.$input.on('autocomplete:selected', spy = jasmine.createSpy()); this.dropdown.trigger('suggestionClicked'); expect(spy).toHaveBeenCalled(); expect(this.input.setQuery).toHaveBeenCalledWith(testDatum.value); expect(this.input.setInputValue) .toHaveBeenCalledWith(testDatum.value, true); var that = this; waitsForAndRuns(function() { return that.dropdown.close.calls.count(); }, done, 100); }); it('should pass the selection method as part of the context ', function(done) { var spy; this.$input.on('autocomplete:selected', spy = jasmine.createSpy()); this.dropdown.trigger('suggestionClicked'); expect(spy).toHaveBeenCalledWith( jasmine.any(Object), undefined, undefined, { selectionMethod: 'click' } ); var that = this; waitsForAndRuns(function() { return that.dropdown.close.calls.count(); }, done, 100); }); }); describe('when dropdown triggers suggestionClicked with undefined displayKey', function() { beforeEach(function() { this.dropdown.getDatumForSuggestion.and.returnValue({}); }); it('should not set input to undefined', function(done) { var spy; this.$input.on('autocomplete:selected', spy = jasmine.createSpy()); this.dropdown.trigger('suggestionClicked'); expect(spy).toHaveBeenCalled(); expect(this.input.setQuery).not.toHaveBeenCalled(); expect(this.input.setInputValue).toHaveBeenCalledWith(undefined, true); var that = this; waitsForAndRuns(function() { return that.dropdown.close.calls.count(); }, done, 100); }); }); describe('when dropdown triggers cursorMoved', function() { beforeEach(function() { this.dropdown.getDatumForCursor.and.returnValue(testDatum); this.dropdown.getCurrentCursor.and.returnValue($('
')); }); it('should update the input value', function() { this.dropdown.trigger('cursorMoved', true); expect(this.input.setInputValue) .toHaveBeenCalledWith(testDatum.value, true); }); it('should update the active descendant', function() { this.dropdown.trigger('cursorMoved', false); expect(this.input.setActiveDescendant) .toHaveBeenCalledWith('option-id'); }); it('should not update the input', function() { this.dropdown.trigger('cursorMoved', false); expect(this.input.setInputValue) .not.toHaveBeenCalled(); }); it('should trigger cursorchanged', function() { var spy; this.$input.on('autocomplete:cursorchanged', spy = jasmine.createSpy()); this.dropdown.trigger('cursorMoved'); expect(spy).toHaveBeenCalled(); }); }); describe('when dropdown triggers cursorRemoved', function() { it('should reset the input value', function() { this.dropdown.trigger('cursorRemoved'); expect(this.input.resetInputValue).toHaveBeenCalled(); }); it('should update the hint', function() { this.dropdown.getDatumForTopSuggestion.and.returnValue(testDatum); this.dropdown.isVisible.and.returnValue(true); this.input.hasOverflow.and.returnValue(false); this.input.getInputValue.and.returnValue(testDatum.value.slice(0, 2)); this.dropdown.trigger('cursorRemoved'); expect(this.input.setHint).toHaveBeenCalledWith(testDatum.value); }); }); describe('when dropdown triggers datasetRendered', function() { it('should update the hint asynchronously', function(done) { this.dropdown.getDatumForTopSuggestion.and.returnValue(testDatum); this.dropdown.isVisible.and.returnValue(true); this.input.hasOverflow.and.returnValue(false); this.input.getInputValue.and.returnValue(testDatum.value.slice(0, 2)); this.dropdown.trigger('datasetRendered'); // ensure it wasn't called synchronously expect(this.input.setHint).not.toHaveBeenCalled(); var that = this; waitsForAndRuns(function() { return !!that.input.setHint.calls.count(); }, function() { expect(that.input.setHint).toHaveBeenCalledWith(testDatum.value); done(); }, 100); }); it('should trigger autocomplete:updated', function(done) { var spy; this.$input.on('autocomplete:updated', spy = jasmine.createSpy()); this.dropdown.trigger('datasetRendered'); var that = this; waitsForAndRuns(function() { return !!that.input.setHint.calls.count(); }, function() { expect(spy).toHaveBeenCalled(); done(); }, 100); }); }); describe('when dropdown triggers opened', function() { it('should update the hint', function() { this.dropdown.getDatumForTopSuggestion.and.returnValue(testDatum); this.dropdown.isVisible.and.returnValue(true); this.input.hasOverflow.and.returnValue(false); this.input.getInputValue.and.returnValue(testDatum.value.slice(0, 2)); this.dropdown.trigger('opened'); expect(this.input.setHint).toHaveBeenCalledWith(testDatum.value); }); it('should trigger autocomplete:opened', function() { var spy; this.$input.on('autocomplete:opened', spy = jasmine.createSpy()); this.dropdown.trigger('opened'); expect(spy).toHaveBeenCalled(); }); it('should trigger autocomplete:shown', function() { var spy; this.$input.on('autocomplete:shown', spy = jasmine.createSpy()); this.dropdown.trigger('shown'); expect(spy).toHaveBeenCalled(); }); it('should trigger autocomplete:redrawn', function() { var spy; this.$input.on('autocomplete:redrawn', spy = jasmine.createSpy()); this.dropdown.trigger('redrawn'); expect(spy).toHaveBeenCalled(); }); it('should set the input\'s aria-expanded to true', function() { this.dropdown.trigger('opened'); expect(this.input.expand).toHaveBeenCalled(); }); }); describe('when dropdown triggers closed', function() { it('should clear the hint', function() { this.dropdown.trigger('closed'); expect(this.input.clearHint).toHaveBeenCalled(); }); it('should trigger autocomplete:closed', function() { var spy; this.$input.on('autocomplete:closed', spy = jasmine.createSpy()); this.dropdown.trigger('closed'); expect(spy).toHaveBeenCalled(); }); it('should set the input\'s aria-expanded to false', function() { this.dropdown.trigger('closed'); expect(this.input.collapse).toHaveBeenCalled(); }); }); describe('when input triggers focused', function() { it('should activate the typeahead', function() { this.input.trigger('focused'); expect(this.view.isActivated).toBe(true); }); it('should not open the dropdown', function() { this.input.trigger('focused'); expect(this.dropdown.open).not.toHaveBeenCalled(); }); }); describe('when input triggers blurred', function() { it('should deactivate the typeahead', function() { this.input.trigger('blurred'); expect(this.view.isActivated).toBe(false); }); it('should empty the dropdown', function() { this.input.trigger('blurred'); expect(this.dropdown.empty).toHaveBeenCalled(); }); it('should close the dropdown', function() { this.input.trigger('blurred'); expect(this.dropdown.close).toHaveBeenCalled(); }); it('should select the suggestion if autoselectOnBlur is true', function() { this.view.autoselectOnBlur = true; this.dropdown.getDatumForTopSuggestion.and.returnValue(testDatum); var spy; this.$input.on('autocomplete:selected', spy = jasmine.createSpy()); this.input.trigger('blurred'); expect(spy).toHaveBeenCalled(); expect(this.input.setQuery).toHaveBeenCalledWith(testDatum.value); expect(this.input.setInputValue) .toHaveBeenCalledWith(testDatum.value, true); }); it('should select the cursor suggestion if autoselectOnBlur is true', function() { this.view.autoselectOnBlur = true; this.dropdown.getDatumForTopSuggestion.and.returnValue(fixtures.data.simple[0]); this.dropdown.getDatumForCursor.and.returnValue(fixtures.data.simple[1]); var spy; this.$input.on('autocomplete:selected', spy = jasmine.createSpy()); this.input.trigger('blurred'); expect(spy).toHaveBeenCalled(); expect(this.input.setQuery).toHaveBeenCalledWith(fixtures.data.simple[1].value); expect(this.input.setInputValue).toHaveBeenCalledWith(fixtures.data.simple[1].value, true); }); it('should pass the selectionMethod as part of the context', function() { this.view.autoselectOnBlur = true; this.dropdown.getDatumForTopSuggestion.and.returnValue(testDatum); var spy; this.$input.on('autocomplete:selected', spy = jasmine.createSpy()); this.input.trigger('blurred'); expect(spy).toHaveBeenCalledWith( jasmine.any(Object), undefined, undefined, { selectionMethod: 'blur' } ); }); }); describe('when debug flag is set', function() { beforeEach(function() { this.view = new Typeahead({ input: this.$input, debug: true, hint: true, datasets: {} }); this.input = this.view.input; }); describe('when input triggers blurred', function() { it('should not empty the dropdown', function() { this.input.trigger('blurred'); expect(this.dropdown.empty).not.toHaveBeenCalled(); }); it('should not close the dropdown', function() { this.input.trigger('blurred'); expect(this.dropdown.close).not.toHaveBeenCalled(); }); }); }); describe('when clearOnSelected flag is set to true', function() { it('clears input when selected', function() { var spy = jasmine.createSpy(); var view = new Typeahead({ input: this.$input, clearOnSelected: true, hint: true, datasets: {} }); view.dropdown.getDatumForCursor.and.returnValue(testDatum); // select something, and clear var $e = jasmine.createSpyObj('event', ['preventDefault']); view.$input.on('autocomplete:selected', spy); view.input.trigger('enterKeyed', $e); expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledWith(jasmine.objectContaining({ type: 'autocomplete:selected', target: jasmine.any(Object), delegateTarget: jasmine.any(Object), currentTarget: jasmine.any(Object), handleObj: jasmine.objectContaining({ type: 'autocomplete:selected' }) }), undefined, undefined, jasmine.any(Object)); expect(view.input.setQuery).toHaveBeenCalledWith(''); expect(view.input.setInputValue).toHaveBeenCalledWith('', true); }); }); describe('when input triggers enterKeyed', function() { beforeEach(function() { this.dropdown.getDatumForCursor.and.returnValue(testDatum); }); it('should select the datum', function(done) { var $e; var spy; $e = jasmine.createSpyObj('event', ['preventDefault']); this.$input.on('autocomplete:selected', spy = jasmine.createSpy()); this.input.trigger('enterKeyed', $e); expect(spy).toHaveBeenCalled(); expect(this.input.setQuery).toHaveBeenCalledWith(testDatum.value); expect(this.input.setInputValue) .toHaveBeenCalledWith(testDatum.value, true); var that = this; waitsForAndRuns(function() { return that.dropdown.close.calls.count(); }, done, 100); }); it('should prevent the default behavior of the event', function() { var $e; $e = jasmine.createSpyObj('event', ['preventDefault']); this.input.trigger('enterKeyed', $e); expect($e.preventDefault).toHaveBeenCalled(); }); it('should pass the selection method as part of the context ', function(done) { var $e; var spy; var anything = jasmine.any(Object); $e = jasmine.createSpyObj('event', ['preventDefault']); this.$input.on('autocomplete:selected', spy = jasmine.createSpy()); this.input.trigger('enterKeyed', $e); expect(spy).toHaveBeenCalledWith( anything, undefined, undefined, {selectionMethod: 'enterKey'} ); var that = this; waitsForAndRuns(function() { return that.dropdown.close.calls.count(); }, done, 100); }); }); describe('when input triggers tabKeyed', function() { describe('when cursor is in use', function() { beforeEach(function() { this.dropdown.getDatumForCursor.and.returnValue(testDatum); }); it('should select the datum', function(done) { var $e; var spy; $e = jasmine.createSpyObj('event', ['preventDefault']); this.$input.on('autocomplete:selected', spy = jasmine.createSpy()); this.input.trigger('tabKeyed', $e); expect(spy).toHaveBeenCalled(); expect(this.input.setQuery).toHaveBeenCalledWith(testDatum.value); expect(this.input.setInputValue) .toHaveBeenCalledWith(testDatum.value, true); var that = this; waitsForAndRuns(function() { return that.dropdown.close.calls.count(); }, done, 100); }); it('should prevent the default behavior of the event', function() { var $e; $e = jasmine.createSpyObj('event', ['preventDefault']); this.input.trigger('tabKeyed', $e); expect($e.preventDefault).toHaveBeenCalled(); }); it('should pass the selectionMethod as part of the context', function(done) { var $e; var spy; $e = jasmine.createSpyObj('event', ['preventDefault']); this.$input.on('autocomplete:selected', spy = jasmine.createSpy()); this.input.trigger('tabKeyed', $e); expect(spy).toHaveBeenCalledWith( jasmine.any(Object), undefined, undefined, {selectionMethod: 'tabKey'} ); var that = this; waitsForAndRuns(function() { return that.dropdown.close.calls.count(); }, done, 100); }); }); describe('when cursor is not in use', function() { it('should autocomplete if tabAutocomplete is true', function() { var spy; this.input.getQuery.and.returnValue('bi'); this.input.getHint.and.returnValue(testDatum.value); this.input.isCursorAtEnd.and.returnValue(true); this.dropdown.getDatumForTopSuggestion.and.returnValue(testDatum); this.$input.on('autocomplete:autocompleted', spy = jasmine.createSpy()); this.input.trigger('tabKeyed'); expect(this.input.setInputValue).toHaveBeenCalledWith(testDatum.value); expect(spy).toHaveBeenCalled(); }); it('should not autocomplete if tabAutocomplete is false', function() { this.view.tabAutocomplete = false; var spy; this.input.getQuery.and.returnValue('bi'); this.input.getHint.and.returnValue(testDatum.value); this.input.isCursorAtEnd.and.returnValue(true); this.dropdown.getDatumForTopSuggestion.and.returnValue(testDatum); this.$input.on('autocomplete:autocompleted', spy = jasmine.createSpy()); this.input.trigger('tabKeyed'); expect(this.input.setInputValue).not.toHaveBeenCalledWith(testDatum.value); expect(spy).not.toHaveBeenCalled(); }); it('should close the dropdown if tabAutocomplete is false', function() { this.view.tabAutocomplete = false; this.input.getQuery.and.returnValue('bi'); this.input.getHint.and.returnValue(testDatum.value); this.input.isCursorAtEnd.and.returnValue(true); this.input.trigger('tabKeyed'); expect(this.dropdown.close).toHaveBeenCalled(); }); }); }); describe('when input triggers escKeyed', function() { it('should close the dropdown', function() { this.input.trigger('escKeyed'); expect(this.dropdown.close).toHaveBeenCalled(); }); it('should reset the input value', function() { this.input.trigger('escKeyed'); expect(this.input.resetInputValue).toHaveBeenCalled(); }); }); describe('when input triggers upKeyed', function() { beforeEach(function() { this.input.getQuery.and.returnValue('ghost'); }); describe('when dropdown is empty and minLength is satisfied', function() { beforeEach(function() { this.dropdown.isEmpty = true; this.view.minLength = 2; this.input.trigger('upKeyed'); }); it('should update dropdown', function() { expect(this.dropdown.update).toHaveBeenCalledWith('ghost'); }); it('should not move cursor up', function() { expect(this.dropdown.moveCursorUp).not.toHaveBeenCalled(); }); }); describe('when dropdown is not empty', function() { beforeEach(function() { this.dropdown.isEmpty = false; this.view.minLength = 2; this.input.trigger('upKeyed'); }); it('should not update dropdown', function() { expect(this.dropdown.update).not.toHaveBeenCalled(); }); it('should move cursor up', function() { expect(this.dropdown.moveCursorUp).toHaveBeenCalled(); }); }); describe('when minLength is not satisfied', function() { beforeEach(function() { this.dropdown.isEmpty = true; this.view.minLength = 10; this.input.trigger('upKeyed'); }); it('should not update dropdown', function() { expect(this.dropdown.update).not.toHaveBeenCalled(); }); it('should move cursor up', function() { expect(this.dropdown.moveCursorUp).toHaveBeenCalled(); }); }); it('should open the dropdown', function() { this.input.trigger('upKeyed'); expect(this.dropdown.open).toHaveBeenCalled(); }); }); describe('when input triggers downKeyed', function() { beforeEach(function() { this.input.getQuery.and.returnValue('ghost'); }); describe('when dropdown is empty and minLength is satisfied', function() { beforeEach(function() { this.dropdown.isEmpty = true; this.view.minLength = 2; this.input.trigger('downKeyed'); }); it('should update dropdown', function() { expect(this.dropdown.update).toHaveBeenCalledWith('ghost'); }); it('should not move cursor down', function() { expect(this.dropdown.moveCursorDown).not.toHaveBeenCalled(); }); }); describe('when dropdown is not empty', function() { beforeEach(function() { this.dropdown.isEmpty = false; this.view.minLength = 2; this.input.trigger('downKeyed'); }); it('should not update dropdown', function() { expect(this.dropdown.update).not.toHaveBeenCalled(); }); it('should move cursor down', function() { expect(this.dropdown.moveCursorDown).toHaveBeenCalled(); }); }); describe('when minLength is not satisfied', function() { beforeEach(function() { this.dropdown.isEmpty = true; this.view.minLength = 10; this.input.trigger('downKeyed'); }); it('should not update dropdown', function() { expect(this.dropdown.update).not.toHaveBeenCalled(); }); it('should move cursor down', function() { expect(this.dropdown.moveCursorDown).toHaveBeenCalled(); }); }); it('should open the dropdown', function() { this.input.trigger('downKeyed'); expect(this.dropdown.open).toHaveBeenCalled(); }); }); describe('when dropdown is empty', function() { it('should trigger autocomplete:empty', function() { var spy; this.$input.on('autocomplete:empty', spy = jasmine.createSpy()); this.dropdown.trigger('empty'); expect(spy).toHaveBeenCalled(); }); }); describe('when input triggers leftKeyed', function() { it('should autocomplete if language is rtl', function() { var spy; this.view.dir = 'rtl'; this.input.getQuery.and.returnValue('bi'); this.input.getHint.and.returnValue(testDatum.value); this.input.isCursorAtEnd.and.returnValue(true); this.dropdown.getDatumForTopSuggestion.and.returnValue(testDatum); this.$input.on('autocomplete:autocompleted', spy = jasmine.createSpy()); this.input.trigger('leftKeyed'); expect(this.input.setInputValue).toHaveBeenCalledWith(testDatum.value); expect(spy).toHaveBeenCalled(); }); }); describe('when input triggers rightKeyed', function() { it('should autocomplete if language is ltr', function() { var spy; this.view.dir = 'ltr'; this.input.getQuery.and.returnValue('bi'); this.input.getHint.and.returnValue(testDatum.value); this.input.isCursorAtEnd.and.returnValue(true); this.dropdown.getDatumForTopSuggestion.and.returnValue(testDatum); this.$input.on('autocomplete:autocompleted', spy = jasmine.createSpy()); this.input.trigger('rightKeyed'); expect(this.input.setInputValue).toHaveBeenCalledWith(testDatum.value); expect(spy).toHaveBeenCalled(); }); }); describe('when input triggers queryChanged', function() { it('should clear the hint if it has become invalid', function() { this.input.trigger('queryChanged', testDatum.value); expect(this.input.clearHintIfInvalid).toHaveBeenCalled(); }); it('should empty dropdown if the query is empty', function() { this.input.trigger('queryChanged', ''); expect(this.dropdown.empty).toHaveBeenCalled(); }); it('should not empty dropdown if the query is non-empty', function() { this.input.trigger('queryChanged', testDatum.value); expect(this.dropdown.empty).not.toHaveBeenCalled(); }); it('should update dropdown', function() { this.input.trigger('queryChanged', testDatum.value); expect(this.dropdown.update).toHaveBeenCalledWith(testDatum.value); }); it('should open the dropdown', function() { this.input.trigger('queryChanged', testDatum.value); expect(this.dropdown.open).toHaveBeenCalled(); }); it('should set the language direction', function() { this.input.getLanguageDirection.and.returnValue('rtl'); this.input.trigger('queryChanged', testDatum.value); expect(this.view.dir).toBe('rtl'); expect(this.view.$node).toHaveCss({direction: 'rtl'}); expect(this.dropdown.setLanguageDirection).toHaveBeenCalledWith('rtl'); }); }); describe('when input triggers whitespaceChanged', function() { it('should update the hint', function() { this.dropdown.getDatumForTopSuggestion.and.returnValue(testDatum); this.dropdown.isVisible.and.returnValue(true); this.input.hasOverflow.and.returnValue(false); this.input.getInputValue.and.returnValue(testDatum.value.slice(0, 2)); this.input.trigger('whitespaceChanged'); expect(this.input.setHint).toHaveBeenCalledWith(testDatum.value); }); it('should open the dropdown', function() { this.input.trigger('whitespaceChanged'); expect(this.dropdown.open).toHaveBeenCalled(); }); }); describe('#open', function() { it('should open the dropdown', function() { this.input.getInputValue.and.returnValue(''); this.view.open(); expect(this.dropdown.open).toHaveBeenCalled(); }); it('should update & open the dropdown if there is a query', function() { this.input.getInputValue.and.returnValue('test'); this.view.open(); expect(this.dropdown.update).toHaveBeenCalled(); expect(this.dropdown.open).toHaveBeenCalled(); }); }); describe('#close', function() { it('should close the dropdown', function() { this.view.close(); expect(this.dropdown.close).toHaveBeenCalled(); }); }); describe('#getVal', function() { it('should return the current query', function() { this.input.getQuery.and.returnValue('woah'); this.view.close(); expect(this.view.getVal()).toBe('woah'); }); }); describe('#setVal', function() { it('should update query', function() { this.view.isActivated = true; this.view.setVal('woah'); expect(this.input.setInputValue).toHaveBeenCalledWith('woah'); }); it('should update query silently if not activated', function() { this.view.setVal('woah'); expect(this.input.setQuery).toHaveBeenCalledWith('woah'); expect(this.input.setInputValue).toHaveBeenCalledWith('woah', true); }); }); describe('#destroy', function() { it('should destroy input', function() { this.view.destroy(); expect(this.input.destroy).toHaveBeenCalled(); }); it('should destroy dropdown', function() { this.view.destroy(); expect(this.dropdown.destroy).toHaveBeenCalled(); }); it('should null out its reference to the wrapper element', function() { this.view.destroy(); expect(this.view.$node).toBeNull(); }); it('should revert DOM changes', function() { this.view.destroy(); // TODO: bad test expect(this.$input).not.toHaveClass('aa-input'); }); }); describe('when instantiated with a custom menu template', function() { beforeEach(function() { appendSetFixtures(fixtures.html.customMenu); this.view.destroy(); this.view = new Typeahead({ input: this.$input, templates: { dropdownMenu: '#my-custom-menu-template' }, datasets: {} }); }); it('should include the template in the menu', function() { var $fixture = $('#jasmine-fixtures'); expect($fixture.find('.aa-dropdown-menu .my-custom-menu').length).toEqual(1); }); }); describe('when instantiated with a custom CSS classes', function() { beforeEach(function() { appendSetFixtures(fixtures.html.customMenu); this.view.destroy(); this.view = new Typeahead({ input: this.$input, hint: true, cssClasses: { root: 'my-root', prefix: 'pp', dropdownMenu: 'my-menu', input: 'my-bar', hint: 'my-clue', suggestions: 'list', suggestion: 'item', cursor: 'pointer', dataset: 'resultset' }, datasets: {} }); }); it('should include the template in the menu', function() { var $fixture = $('#jasmine-fixtures'); expect($fixture.find('.my-root').length).toEqual(1); expect($fixture.find('.my-root .pp-my-menu').length).toEqual(1); expect($fixture.find('.my-root .pp-my-bar').length).toEqual(1); expect($fixture.find('.my-root .pp-my-clue').length).toEqual(1); }); }); describe('when instantiated with a custom menu container', function() { beforeEach(function() { appendSetFixtures(fixtures.html.customMenuContainer); this.view.destroy(); this.view = new Typeahead({ input: this.$input, dropdownMenuContainer: '#custom-menu-container', datasets: {} }); }); it('should include the template in the menu', function() { var $fixture = $('#custom-menu-container'); expect($fixture.find('.aa-dropdown-menu').length).toEqual(1); }); }); describe('when openOnFocus is set', function() { beforeEach(function() { appendSetFixtures(fixtures.html.customMenu); this.view.destroy(); this.view = new Typeahead({ input: this.$input, openOnFocus: true, minLength: 0, datasets: {} }); this.input = this.view.input; }); it('should open the dropdown', function() { this.input.getQuery.and.returnValue(''); this.input.trigger('focused'); expect(this.view.dropdown.open).toHaveBeenCalled(); }); }); describe('when set autoWidth option', function() { it('should set default to true', function() { this.dropdown.trigger('redrawn'); expect(this.view.autoWidth).toBeTruthy(); expect(/\d{3}px/.test(this.view.$node[0].style.width)).toBeTruthy(); }); it('should not put width style when autoWidth is false', function() { this.view.autoWidth = false; this.dropdown.trigger('redrawn'); expect(this.view.autoWidth).toBeFalsy(); expect(this.view.$node[0].style.width).toBeFalsy(); }); }); describe('when aria-label is set', function() { beforeEach(function() { this.view.destroy(); }); it('should set aria-label to the specified string', function() { this.view = new Typeahead({ input: this.$input, ariaLabel: 'custom-aria-label' }); expect(this.$input.attr('aria-label')).toBe('custom-aria-label'); }); it('should not set an aria-label if no value is specified', function() { this.view = new Typeahead({ input: this.$input }); expect(this.$input.attr('aria-label')).toBeUndefined(); }); }); }); ================================================ FILE: js/autocomplete.js/test/unit/utils_spec.js ================================================ 'use strict'; /* eslint-env mocha, jasmine */ var _ = require('../../src/common/utils.js'); describe('escapeHTML', function() { it('should escape HTML but preserve the default tags', function() { var test = '' + 'OTHER CONTENTVALUE2OTHER CONTENT$'; var actual = _.escapeHighlightedString(test); expect(actual).toEqual('<img src=VALUE1 onerror=alert(1) />OTHER CONTENTVALUE2OTHER CONTENT$'); }); it('should escape HTML but preserve the default tags when using custom tags', function() { var test = '' + 'OTHER CONTENTVALUE2OTHER CONTENT$'; var actual = _.escapeHighlightedString(test, '', ''); expect(actual).toEqual('<img src=VALUE1 onerror=alert(1) />OTHER CONTENTVALUE2OTHER CONTENT$'); }); it('should report the isMsie state correctly', function() { var actual = _.isMsie(); expect(actual).toEqual(false); }); it('should report the isMsie state correctly under a non-IE browser', function() { var ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36'; var actual = _.isMsie(ua); expect(actual).toEqual(false); }); it('should report the isMsie state correctly under an IE browser', function() { var ua = 'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko'; var actual = _.isMsie(ua); expect(actual).toEqual('11.0'); }); it('should report the isMsie state correctly under a browser that includes Trident but is not IE', function() { var ua = 'Mozilla/5.0 (iPad; CPU OS 11_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15G77 KurogoVersion/2.7.7 (Kurogo iOS Tablet) KurogoOSVersion/11.4.1 KurogoAppVersion/2.0.1 (com.telerik.TridentUniversity)'; var actual = _.isMsie(ua); expect(actual).toEqual(false); }); }); describe('every', function() { // simulating an implementation of Array.prototype.each _.each = function(obj, callback) { for (var i = 0; i < obj.length; i++) { callback(obj[i], i, _); // note that we do not return here to break for loop, angular does not do this } }; it('_.every should return false when at least one result is false', function() { expect( _.every([ {isEmpty: true}, {isEmpty: false} ], function(dataset) { return dataset.isEmpty; })).toEqual(false); }); it('_.every should return false when each result is false', function() { expect( _.every([ {isEmpty: false}, {isEmpty: false} ], function(dataset) { return dataset.isEmpty; })).toEqual(false); }); it('_.every should return true when all results are true', function() { expect( _.every([ {isEmpty: true}, {isEmpty: true} ], function(dataset) { return dataset.isEmpty; })).toEqual(true); }); }); ================================================ FILE: js/autocomplete.js/version.js ================================================ module.exports = "0.38.1"; ================================================ FILE: js/autocomplete.js/zepto.js ================================================ /* istanbul ignore next */ /* Zepto v1.2.0 - zepto event assets data - zeptojs.com/license */ (function(global, factory) { module.exports = factory(global); }(/* this ##### UPDATED: here we want to use window/global instead of this which is the current file context ##### */ window, function(window) { var Zepto = (function() { var undefined, key, $, classList, emptyArray = [], concat = emptyArray.concat, filter = emptyArray.filter, slice = emptyArray.slice, document = window.document, elementDisplay = {}, classCache = {}, cssNumber = { 'column-count': 1, 'columns': 1, 'font-weight': 1, 'line-height': 1,'opacity': 1, 'z-index': 1, 'zoom': 1 }, fragmentRE = /^\s*<(\w+|!)[^>]*>/, singleTagRE = /^<(\w+)\s*\/?>(?:<\/\1>|)$/, tagExpanderRE = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig, rootNodeRE = /^(?:body|html)$/i, capitalRE = /([A-Z])/g, // special attributes that should be get/set via method calls methodAttributes = ['val', 'css', 'html', 'text', 'data', 'width', 'height', 'offset'], adjacencyOperators = [ 'after', 'prepend', 'before', 'append' ], table = document.createElement('table'), tableRow = document.createElement('tr'), containers = { 'tr': document.createElement('tbody'), 'tbody': table, 'thead': table, 'tfoot': table, 'td': tableRow, 'th': tableRow, '*': document.createElement('div') }, readyRE = /complete|loaded|interactive/, simpleSelectorRE = /^[\w-]*$/, class2type = {}, toString = class2type.toString, zepto = {}, camelize, uniq, tempParent = document.createElement('div'), propMap = { 'tabindex': 'tabIndex', 'readonly': 'readOnly', 'for': 'htmlFor', 'class': 'className', 'maxlength': 'maxLength', 'cellspacing': 'cellSpacing', 'cellpadding': 'cellPadding', 'rowspan': 'rowSpan', 'colspan': 'colSpan', 'usemap': 'useMap', 'frameborder': 'frameBorder', 'contenteditable': 'contentEditable' }, isArray = Array.isArray || function(object){ return object instanceof Array } zepto.matches = function(element, selector) { if (!selector || !element || element.nodeType !== 1) return false var matchesSelector = element.matches || element.webkitMatchesSelector || element.mozMatchesSelector || element.oMatchesSelector || element.matchesSelector if (matchesSelector) return matchesSelector.call(element, selector) // fall back to performing a selector: var match, parent = element.parentNode, temp = !parent if (temp) (parent = tempParent).appendChild(element) match = ~zepto.qsa(parent, selector).indexOf(element) temp && tempParent.removeChild(element) return match } function type(obj) { return obj == null ? String(obj) : class2type[toString.call(obj)] || "object" } function isFunction(value) { return type(value) == "function" } function isWindow(obj) { return obj != null && obj == obj.window } function isDocument(obj) { return obj != null && obj.nodeType == obj.DOCUMENT_NODE } function isObject(obj) { return type(obj) == "object" } function isPlainObject(obj) { return isObject(obj) && !isWindow(obj) && Object.getPrototypeOf(obj) == Object.prototype } function likeArray(obj) { var length = !!obj && 'length' in obj && obj.length, type = $.type(obj) return 'function' != type && !isWindow(obj) && ( 'array' == type || length === 0 || (typeof length == 'number' && length > 0 && (length - 1) in obj) ) } function compact(array) { return filter.call(array, function(item){ return item != null }) } function flatten(array) { return array.length > 0 ? $.fn.concat.apply([], array) : array } camelize = function(str){ return str.replace(/-+(.)?/g, function(match, chr){ return chr ? chr.toUpperCase() : '' }) } function dasherize(str) { return str.replace(/::/g, '/') .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2') .replace(/([a-z\d])([A-Z])/g, '$1_$2') .replace(/_/g, '-') .toLowerCase() } uniq = function(array){ return filter.call(array, function(item, idx){ return array.indexOf(item) == idx }) } function classRE(name) { return name in classCache ? classCache[name] : (classCache[name] = new RegExp('(^|\\s)' + name + '(\\s|$)')) } function maybeAddPx(name, value) { return (typeof value == "number" && !cssNumber[dasherize(name)]) ? value + "px" : value } function defaultDisplay(nodeName) { var element, display if (!elementDisplay[nodeName]) { element = document.createElement(nodeName) document.body.appendChild(element) display = getComputedStyle(element, '').getPropertyValue("display") element.parentNode.removeChild(element) display == "none" && (display = "block") elementDisplay[nodeName] = display } return elementDisplay[nodeName] } function children(element) { return 'children' in element ? slice.call(element.children) : $.map(element.childNodes, function(node){ if (node.nodeType == 1) return node }) } function Z(dom, selector) { var i, len = dom ? dom.length : 0 for (i = 0; i < len; i++) this[i] = dom[i] this.length = len this.selector = selector || '' } // `$.zepto.fragment` takes a html string and an optional tag name // to generate DOM nodes from the given html string. // The generated DOM nodes are returned as an array. // This function can be overridden in plugins for example to make // it compatible with browsers that don't support the DOM fully. zepto.fragment = function(html, name, properties) { var dom, nodes, container // A special case optimization for a single tag if (singleTagRE.test(html)) dom = $(document.createElement(RegExp.$1)) if (!dom) { if (html.replace) html = html.replace(tagExpanderRE, "<$1>") if (name === undefined) name = fragmentRE.test(html) && RegExp.$1 if (!(name in containers)) name = '*' container = containers[name] container.innerHTML = '' + html dom = $.each(slice.call(container.childNodes), function(){ container.removeChild(this) }) } if (isPlainObject(properties)) { nodes = $(dom) $.each(properties, function(key, value) { if (methodAttributes.indexOf(key) > -1) nodes[key](value) else nodes.attr(key, value) }) } return dom } // `$.zepto.Z` swaps out the prototype of the given `dom` array // of nodes with `$.fn` and thus supplying all the Zepto functions // to the array. This method can be overridden in plugins. zepto.Z = function(dom, selector) { return new Z(dom, selector) } // `$.zepto.isZ` should return `true` if the given object is a Zepto // collection. This method can be overridden in plugins. zepto.isZ = function(object) { return object instanceof zepto.Z } // `$.zepto.init` is Zepto's counterpart to jQuery's `$.fn.init` and // takes a CSS selector and an optional context (and handles various // special cases). // This method can be overridden in plugins. zepto.init = function(selector, context) { var dom // If nothing given, return an empty Zepto collection if (!selector) return zepto.Z() // Optimize for string selectors else if (typeof selector == 'string') { selector = selector.trim() // If it's a html fragment, create nodes from it // Note: In both Chrome 21 and Firefox 15, DOM error 12 // is thrown if the fragment doesn't begin with < if (selector[0] == '<' && fragmentRE.test(selector)) dom = zepto.fragment(selector, RegExp.$1, context), selector = null // If there's a context, create a collection on that context first, and select // nodes from there else if (context !== undefined) return $(context).find(selector) // If it's a CSS selector, use it to select nodes. else dom = zepto.qsa(document, selector) } // If a function is given, call it when the DOM is ready else if (isFunction(selector)) return $(document).ready(selector) // If a Zepto collection is given, just return it else if (zepto.isZ(selector)) return selector else { // normalize array if an array of nodes is given if (isArray(selector)) dom = compact(selector) // Wrap DOM nodes. else if (isObject(selector)) dom = [selector], selector = null // If it's a html fragment, create nodes from it else if (fragmentRE.test(selector)) dom = zepto.fragment(selector.trim(), RegExp.$1, context), selector = null // If there's a context, create a collection on that context first, and select // nodes from there else if (context !== undefined) return $(context).find(selector) // And last but no least, if it's a CSS selector, use it to select nodes. else dom = zepto.qsa(document, selector) } // create a new Zepto collection from the nodes found return zepto.Z(dom, selector) } // `$` will be the base `Zepto` object. When calling this // function just call `$.zepto.init, which makes the implementation // details of selecting nodes and creating Zepto collections // patchable in plugins. $ = function(selector, context){ return zepto.init(selector, context) } function extend(target, source, deep) { for (key in source) if (deep && (isPlainObject(source[key]) || isArray(source[key]))) { if (isPlainObject(source[key]) && !isPlainObject(target[key])) target[key] = {} if (isArray(source[key]) && !isArray(target[key])) target[key] = [] extend(target[key], source[key], deep) } else if (source[key] !== undefined) target[key] = source[key] } // Copy all but undefined properties from one or more // objects to the `target` object. $.extend = function(target){ var deep, args = slice.call(arguments, 1) if (typeof target == 'boolean') { deep = target target = args.shift() } args.forEach(function(arg){ extend(target, arg, deep) }) return target } // `$.zepto.qsa` is Zepto's CSS selector implementation which // uses `document.querySelectorAll` and optimizes for some special cases, like `#id`. // This method can be overridden in plugins. zepto.qsa = function(element, selector){ var found, maybeID = selector[0] == '#', maybeClass = !maybeID && selector[0] == '.', nameOnly = maybeID || maybeClass ? selector.slice(1) : selector, // Ensure that a 1 char tag name still gets checked isSimple = simpleSelectorRE.test(nameOnly) return (element.getElementById && isSimple && maybeID) ? // Safari DocumentFragment doesn't have getElementById ( (found = element.getElementById(nameOnly)) ? [found] : [] ) : (element.nodeType !== 1 && element.nodeType !== 9 && element.nodeType !== 11) ? [] : slice.call( isSimple && !maybeID && element.getElementsByClassName ? // DocumentFragment doesn't have getElementsByClassName/TagName maybeClass ? element.getElementsByClassName(nameOnly) : // If it's simple, it could be a class element.getElementsByTagName(selector) : // Or a tag element.querySelectorAll(selector) // Or it's not simple, and we need to query all ) } function filtered(nodes, selector) { return selector == null ? $(nodes) : $(nodes).filter(selector) } $.contains = document.documentElement.contains ? function(parent, node) { return parent !== node && parent.contains(node) } : function(parent, node) { while (node && (node = node.parentNode)) if (node === parent) return true return false } function funcArg(context, arg, idx, payload) { return isFunction(arg) ? arg.call(context, idx, payload) : arg } function setAttribute(node, name, value) { value == null ? node.removeAttribute(name) : node.setAttribute(name, value) } // access className property while respecting SVGAnimatedString function className(node, value){ var klass = node.className || '', svg = klass && klass.baseVal !== undefined if (value === undefined) return svg ? klass.baseVal : klass svg ? (klass.baseVal = value) : (node.className = value) } // "true" => true // "false" => false // "null" => null // "42" => 42 // "42.5" => 42.5 // "08" => "08" // JSON => parse if valid // String => self function deserializeValue(value) { try { return value ? value == "true" || ( value == "false" ? false : value == "null" ? null : +value + "" == value ? +value : /^[\[\{]/.test(value) ? $.parseJSON(value) : value ) : value } catch(e) { return value } } $.type = type $.isFunction = isFunction $.isWindow = isWindow $.isArray = isArray $.isPlainObject = isPlainObject $.isEmptyObject = function(obj) { var name for (name in obj) return false return true } $.isNumeric = function(val) { var num = Number(val), type = typeof val return val != null && type != 'boolean' && (type != 'string' || val.length) && !isNaN(num) && isFinite(num) || false } $.inArray = function(elem, array, i){ return emptyArray.indexOf.call(array, elem, i) } $.camelCase = camelize $.trim = function(str) { return str == null ? "" : String.prototype.trim.call(str) } // plugin compatibility $.uuid = 0 $.support = { } $.expr = { } $.noop = function() {} $.map = function(elements, callback){ var value, values = [], i, key if (likeArray(elements)) for (i = 0; i < elements.length; i++) { value = callback(elements[i], i) if (value != null) values.push(value) } else for (key in elements) { value = callback(elements[key], key) if (value != null) values.push(value) } return flatten(values) } $.each = function(elements, callback){ var i, key if (likeArray(elements)) { for (i = 0; i < elements.length; i++) if (callback.call(elements[i], i, elements[i]) === false) return elements } else { for (key in elements) if (callback.call(elements[key], key, elements[key]) === false) return elements } return elements } $.grep = function(elements, callback){ return filter.call(elements, callback) } if (window.JSON) $.parseJSON = JSON.parse // Populate the class2type map $.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) { class2type[ "[object " + name + "]" ] = name.toLowerCase() }) // Define methods that will be available on all // Zepto collections $.fn = { constructor: zepto.Z, length: 0, // Because a collection acts like an array // copy over these useful array functions. forEach: emptyArray.forEach, reduce: emptyArray.reduce, push: emptyArray.push, sort: emptyArray.sort, splice: emptyArray.splice, indexOf: emptyArray.indexOf, concat: function(){ var i, value, args = [] for (i = 0; i < arguments.length; i++) { value = arguments[i] args[i] = zepto.isZ(value) ? value.toArray() : value } return concat.apply(zepto.isZ(this) ? this.toArray() : this, args) }, // `map` and `slice` in the jQuery API work differently // from their array counterparts map: function(fn){ return $($.map(this, function(el, i){ return fn.call(el, i, el) })) }, slice: function(){ return $(slice.apply(this, arguments)) }, ready: function(callback){ // need to check if document.body exists for IE as that browser reports // document ready when it hasn't yet created the body element if (readyRE.test(document.readyState) && document.body) callback($) else document.addEventListener('DOMContentLoaded', function(){ callback($) }, false) return this }, get: function(idx){ return idx === undefined ? slice.call(this) : this[idx >= 0 ? idx : idx + this.length] }, toArray: function(){ return this.get() }, size: function(){ return this.length }, remove: function(){ return this.each(function(){ if (this.parentNode != null) this.parentNode.removeChild(this) }) }, each: function(callback){ emptyArray.every.call(this, function(el, idx){ return callback.call(el, idx, el) !== false }) return this }, filter: function(selector){ if (isFunction(selector)) return this.not(this.not(selector)) return $(filter.call(this, function(element){ return zepto.matches(element, selector) })) }, add: function(selector,context){ return $(uniq(this.concat($(selector,context)))) }, is: function(selector){ return this.length > 0 && zepto.matches(this[0], selector) }, not: function(selector){ var nodes=[] if (isFunction(selector) && selector.call !== undefined) this.each(function(idx){ if (!selector.call(this,idx)) nodes.push(this) }) else { var excludes = typeof selector == 'string' ? this.filter(selector) : (likeArray(selector) && isFunction(selector.item)) ? slice.call(selector) : $(selector) this.forEach(function(el){ if (excludes.indexOf(el) < 0) nodes.push(el) }) } return $(nodes) }, has: function(selector){ return this.filter(function(){ return isObject(selector) ? $.contains(this, selector) : $(this).find(selector).size() }) }, eq: function(idx){ return idx === -1 ? this.slice(idx) : this.slice(idx, + idx + 1) }, first: function(){ var el = this[0] return el && !isObject(el) ? el : $(el) }, last: function(){ var el = this[this.length - 1] return el && !isObject(el) ? el : $(el) }, find: function(selector){ var result, $this = this if (!selector) result = $() else if (typeof selector == 'object') result = $(selector).filter(function(){ var node = this return emptyArray.some.call($this, function(parent){ return $.contains(parent, node) }) }) else if (this.length == 1) result = $(zepto.qsa(this[0], selector)) else result = this.map(function(){ return zepto.qsa(this, selector) }) return result }, closest: function(selector, context){ var nodes = [], collection = typeof selector == 'object' && $(selector) this.each(function(_, node){ while (node && !(collection ? collection.indexOf(node) >= 0 : zepto.matches(node, selector))) node = node !== context && !isDocument(node) && node.parentNode if (node && nodes.indexOf(node) < 0) nodes.push(node) }) return $(nodes) }, parents: function(selector){ var ancestors = [], nodes = this while (nodes.length > 0) nodes = $.map(nodes, function(node){ if ((node = node.parentNode) && !isDocument(node) && ancestors.indexOf(node) < 0) { ancestors.push(node) return node } }) return filtered(ancestors, selector) }, parent: function(selector){ return filtered(uniq(this.pluck('parentNode')), selector) }, children: function(selector){ return filtered(this.map(function(){ return children(this) }), selector) }, contents: function() { return this.map(function() { return this.contentDocument || slice.call(this.childNodes) }) }, siblings: function(selector){ return filtered(this.map(function(i, el){ return filter.call(children(el.parentNode), function(child){ return child!==el }) }), selector) }, empty: function(){ return this.each(function(){ this.innerHTML = '' }) }, // `pluck` is borrowed from Prototype.js pluck: function(property){ return $.map(this, function(el){ return el[property] }) }, show: function(){ return this.each(function(){ this.style.display == "none" && (this.style.display = '') if (getComputedStyle(this, '').getPropertyValue("display") == "none") this.style.display = defaultDisplay(this.nodeName) }) }, replaceWith: function(newContent){ return this.before(newContent).remove() }, wrap: function(structure){ var func = isFunction(structure) if (this[0] && !func) var dom = $(structure).get(0), clone = dom.parentNode || this.length > 1 return this.each(function(index){ $(this).wrapAll( func ? structure.call(this, index) : clone ? dom.cloneNode(true) : dom ) }) }, wrapAll: function(structure){ if (this[0]) { $(this[0]).before(structure = $(structure)) var children // drill down to the inmost element while ((children = structure.children()).length) structure = children.first() $(structure).append(this) } return this }, wrapInner: function(structure){ var func = isFunction(structure) return this.each(function(index){ var self = $(this), contents = self.contents(), dom = func ? structure.call(this, index) : structure contents.length ? contents.wrapAll(dom) : self.append(dom) }) }, unwrap: function(){ this.parent().each(function(){ $(this).replaceWith($(this).children()) }) return this }, clone: function(){ return this.map(function(){ return this.cloneNode(true) }) }, hide: function(){ return this.css("display", "none") }, toggle: function(setting){ return this.each(function(){ var el = $(this) ;(setting === undefined ? el.css("display") == "none" : setting) ? el.show() : el.hide() }) }, prev: function(selector){ return $(this.pluck('previousElementSibling')).filter(selector || '*') }, next: function(selector){ return $(this.pluck('nextElementSibling')).filter(selector || '*') }, html: function(html){ return 0 in arguments ? this.each(function(idx){ var originHtml = this.innerHTML $(this).empty().append( funcArg(this, html, idx, originHtml) ) }) : (0 in this ? this[0].innerHTML : null) }, text: function(text){ return 0 in arguments ? this.each(function(idx){ var newText = funcArg(this, text, idx, this.textContent) this.textContent = newText == null ? '' : ''+newText }) : (0 in this ? this.pluck('textContent').join("") : null) }, attr: function(name, value){ var result return (typeof name == 'string' && !(1 in arguments)) ? (0 in this && this[0].nodeType == 1 && (result = this[0].getAttribute(name)) != null ? result : undefined) : this.each(function(idx){ if (this.nodeType !== 1) return if (isObject(name)) for (key in name) setAttribute(this, key, name[key]) else setAttribute(this, name, funcArg(this, value, idx, this.getAttribute(name))) }) }, removeAttr: function(name){ return this.each(function(){ this.nodeType === 1 && name.split(' ').forEach(function(attribute){ setAttribute(this, attribute) }, this)}) }, prop: function(name, value){ name = propMap[name] || name return (1 in arguments) ? this.each(function(idx){ this[name] = funcArg(this, value, idx, this[name]) }) : (this[0] && this[0][name]) }, removeProp: function(name){ name = propMap[name] || name return this.each(function(){ delete this[name] }) }, data: function(name, value){ var attrName = 'data-' + name.replace(capitalRE, '-$1').toLowerCase() var data = (1 in arguments) ? this.attr(attrName, value) : this.attr(attrName) return data !== null ? deserializeValue(data) : undefined }, val: function(value){ if (0 in arguments) { if (value == null) value = "" return this.each(function(idx){ this.value = funcArg(this, value, idx, this.value) }) } else { return this[0] && (this[0].multiple ? $(this[0]).find('option').filter(function(){ return this.selected }).pluck('value') : this[0].value) } }, offset: function(coordinates){ if (coordinates) return this.each(function(index){ var $this = $(this), coords = funcArg(this, coordinates, index, $this.offset()), parentOffset = $this.offsetParent().offset(), props = { top: coords.top - parentOffset.top, left: coords.left - parentOffset.left } if ($this.css('position') == 'static') props['position'] = 'relative' $this.css(props) }) if (!this.length) return null if (document.documentElement !== this[0] && !$.contains(document.documentElement, this[0])) return {top: 0, left: 0} var obj = this[0].getBoundingClientRect() return { left: obj.left + window.pageXOffset, top: obj.top + window.pageYOffset, width: Math.round(obj.width), height: Math.round(obj.height) } }, css: function(property, value){ if (arguments.length < 2) { var element = this[0] if (typeof property == 'string') { if (!element) return return element.style[camelize(property)] || getComputedStyle(element, '').getPropertyValue(property) } else if (isArray(property)) { if (!element) return var props = {} var computedStyle = getComputedStyle(element, '') $.each(property, function(_, prop){ props[prop] = (element.style[camelize(prop)] || computedStyle.getPropertyValue(prop)) }) return props } } var css = '' if (type(property) == 'string') { if (!value && value !== 0) this.each(function(){ this.style.removeProperty(dasherize(property)) }) else css = dasherize(property) + ":" + maybeAddPx(property, value) } else { for (key in property) if (!property[key] && property[key] !== 0) this.each(function(){ this.style.removeProperty(dasherize(key)) }) else css += dasherize(key) + ':' + maybeAddPx(key, property[key]) + ';' } return this.each(function(){ this.style.cssText += ';' + css }) }, index: function(element){ return element ? this.indexOf($(element)[0]) : this.parent().children().indexOf(this[0]) }, hasClass: function(name){ if (!name) return false return emptyArray.some.call(this, function(el){ return this.test(className(el)) }, classRE(name)) }, addClass: function(name){ if (!name) return this return this.each(function(idx){ if (!('className' in this)) return classList = [] var cls = className(this), newName = funcArg(this, name, idx, cls) newName.split(/\s+/g).forEach(function(klass){ if (!$(this).hasClass(klass)) classList.push(klass) }, this) classList.length && className(this, cls + (cls ? " " : "") + classList.join(" ")) }) }, removeClass: function(name){ return this.each(function(idx){ if (!('className' in this)) return if (name === undefined) return className(this, '') classList = className(this) funcArg(this, name, idx, classList).split(/\s+/g).forEach(function(klass){ classList = classList.replace(classRE(klass), " ") }) className(this, classList.trim()) }) }, toggleClass: function(name, when){ if (!name) return this return this.each(function(idx){ var $this = $(this), names = funcArg(this, name, idx, className(this)) names.split(/\s+/g).forEach(function(klass){ (when === undefined ? !$this.hasClass(klass) : when) ? $this.addClass(klass) : $this.removeClass(klass) }) }) }, scrollTop: function(value){ if (!this.length) return var hasScrollTop = 'scrollTop' in this[0] if (value === undefined) return hasScrollTop ? this[0].scrollTop : this[0].pageYOffset return this.each(hasScrollTop ? function(){ this.scrollTop = value } : function(){ this.scrollTo(this.scrollX, value) }) }, scrollLeft: function(value){ if (!this.length) return var hasScrollLeft = 'scrollLeft' in this[0] if (value === undefined) return hasScrollLeft ? this[0].scrollLeft : this[0].pageXOffset return this.each(hasScrollLeft ? function(){ this.scrollLeft = value } : function(){ this.scrollTo(value, this.scrollY) }) }, position: function() { if (!this.length) return var elem = this[0], // Get *real* offsetParent offsetParent = this.offsetParent(), // Get correct offsets offset = this.offset(), parentOffset = rootNodeRE.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset() // Subtract element margins // note: when an element has margin: auto the offsetLeft and marginLeft // are the same in Safari causing offset.left to incorrectly be 0 offset.top -= parseFloat( $(elem).css('margin-top') ) || 0 offset.left -= parseFloat( $(elem).css('margin-left') ) || 0 // Add offsetParent borders parentOffset.top += parseFloat( $(offsetParent[0]).css('border-top-width') ) || 0 parentOffset.left += parseFloat( $(offsetParent[0]).css('border-left-width') ) || 0 // Subtract the two offsets return { top: offset.top - parentOffset.top, left: offset.left - parentOffset.left } }, offsetParent: function() { return this.map(function(){ var parent = this.offsetParent || document.body while (parent && !rootNodeRE.test(parent.nodeName) && $(parent).css("position") == "static") parent = parent.offsetParent return parent }) } } // for now $.fn.detach = $.fn.remove // Generate the `width` and `height` functions ;['width', 'height'].forEach(function(dimension){ var dimensionProperty = dimension.replace(/./, function(m){ return m[0].toUpperCase() }) $.fn[dimension] = function(value){ var offset, el = this[0] if (value === undefined) return isWindow(el) ? el['inner' + dimensionProperty] : isDocument(el) ? el.documentElement['scroll' + dimensionProperty] : (offset = this.offset()) && offset[dimension] else return this.each(function(idx){ el = $(this) el.css(dimension, funcArg(this, value, idx, el[dimension]())) }) } }) function traverseNode(node, fun) { fun(node) for (var i = 0, len = node.childNodes.length; i < len; i++) traverseNode(node.childNodes[i], fun) } // Generate the `after`, `prepend`, `before`, `append`, // `insertAfter`, `insertBefore`, `appendTo`, and `prependTo` methods. adjacencyOperators.forEach(function(operator, operatorIndex) { var inside = operatorIndex % 2 //=> prepend, append $.fn[operator] = function(){ // arguments can be nodes, arrays of nodes, Zepto objects and HTML strings var argType, nodes = $.map(arguments, function(arg) { var arr = [] argType = type(arg) if (argType == "array") { arg.forEach(function(el) { if (el.nodeType !== undefined) return arr.push(el) else if ($.zepto.isZ(el)) return arr = arr.concat(el.get()) arr = arr.concat(zepto.fragment(el)) }) return arr } return argType == "object" || arg == null ? arg : zepto.fragment(arg) }), parent, copyByClone = this.length > 1 if (nodes.length < 1) return this return this.each(function(_, target){ parent = inside ? target : target.parentNode // convert all methods to a "before" operation target = operatorIndex == 0 ? target.nextSibling : operatorIndex == 1 ? target.firstChild : operatorIndex == 2 ? target : null var parentInDocument = $.contains(document.documentElement, parent) nodes.forEach(function(node){ if (copyByClone) node = node.cloneNode(true) else if (!parent) return $(node).remove() parent.insertBefore(node, target) if (parentInDocument) traverseNode(node, function(el){ if (el.nodeName != null && el.nodeName.toUpperCase() === 'SCRIPT' && (!el.type || el.type === 'text/javascript') && !el.src){ var target = el.ownerDocument ? el.ownerDocument.defaultView : window target['eval'].call(target, el.innerHTML) } }) }) }) } // after => insertAfter // prepend => prependTo // before => insertBefore // append => appendTo $.fn[inside ? operator+'To' : 'insert'+(operatorIndex ? 'Before' : 'After')] = function(html){ $(html)[operator](this) return this } }) zepto.Z.prototype = Z.prototype = $.fn // Export internal API functions in the `$.zepto` namespace zepto.uniq = uniq zepto.deserializeValue = deserializeValue $.zepto = zepto return $ })() ;(function($){ var _zid = 1, undefined, slice = Array.prototype.slice, isFunction = $.isFunction, isString = function(obj){ return typeof obj == 'string' }, handlers = {}, specialEvents={}, focusinSupported = 'onfocusin' in window, focus = { focus: 'focusin', blur: 'focusout' }, hover = { mouseenter: 'mouseover', mouseleave: 'mouseout' } specialEvents.click = specialEvents.mousedown = specialEvents.mouseup = specialEvents.mousemove = 'MouseEvents' function zid(element) { return element._zid || (element._zid = _zid++) } function findHandlers(element, event, fn, selector) { event = parse(event) if (event.ns) var matcher = matcherFor(event.ns) return (handlers[zid(element)] || []).filter(function(handler) { return handler && (!event.e || handler.e == event.e) && (!event.ns || matcher.test(handler.ns)) && (!fn || zid(handler.fn) === zid(fn)) && (!selector || handler.sel == selector) }) } function parse(event) { var parts = ('' + event).split('.') return {e: parts[0], ns: parts.slice(1).sort().join(' ')} } function matcherFor(ns) { return new RegExp('(?:^| )' + ns.replace(' ', ' .* ?') + '(?: |$)') } function eventCapture(handler, captureSetting) { return handler.del && (!focusinSupported && (handler.e in focus)) || !!captureSetting } function realEvent(type) { return hover[type] || (focusinSupported && focus[type]) || type } function add(element, events, fn, data, selector, delegator, capture){ var id = zid(element), set = (handlers[id] || (handlers[id] = [])) events.split(/\s/).forEach(function(event){ if (event == 'ready') return $(document).ready(fn) var handler = parse(event) handler.fn = fn handler.sel = selector // emulate mouseenter, mouseleave if (handler.e in hover) fn = function(e){ var related = e.relatedTarget if (!related || (related !== this && !$.contains(this, related))) return handler.fn.apply(this, arguments) } handler.del = delegator var callback = delegator || fn handler.proxy = function(e){ e = compatible(e) if (e.isImmediatePropagationStopped()) return try { var dataPropDescriptor = Object.getOwnPropertyDescriptor(e, 'data') if (!dataPropDescriptor || dataPropDescriptor.writable) e.data = data } catch (e) {} // when using strict mode dataPropDescriptor will be undefined when e is InputEvent (even though data property exists). So we surround with try/catch var result = callback.apply(element, e._args == undefined ? [e] : [e].concat(e._args)) if (result === false) e.preventDefault(), e.stopPropagation() return result } handler.i = set.length set.push(handler) if ('addEventListener' in element) element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture)) }) } function remove(element, events, fn, selector, capture){ var id = zid(element) ;(events || '').split(/\s/).forEach(function(event){ findHandlers(element, event, fn, selector).forEach(function(handler){ delete handlers[id][handler.i] if ('removeEventListener' in element) element.removeEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture)) }) }) } $.event = { add: add, remove: remove } $.proxy = function(fn, context) { var args = (2 in arguments) && slice.call(arguments, 2) if (isFunction(fn)) { var proxyFn = function(){ return fn.apply(context, args ? args.concat(slice.call(arguments)) : arguments) } proxyFn._zid = zid(fn) return proxyFn } else if (isString(context)) { if (args) { args.unshift(fn[context], fn) return $.proxy.apply(null, args) } else { return $.proxy(fn[context], fn) } } else { throw new TypeError("expected function") } } $.fn.bind = function(event, data, callback){ return this.on(event, data, callback) } $.fn.unbind = function(event, callback){ return this.off(event, callback) } $.fn.one = function(event, selector, data, callback){ return this.on(event, selector, data, callback, 1) } var returnTrue = function(){return true}, returnFalse = function(){return false}, ignoreProperties = /^([A-Z]|returnValue$|layer[XY]$|webkitMovement[XY]$)/, eventMethods = { preventDefault: 'isDefaultPrevented', stopImmediatePropagation: 'isImmediatePropagationStopped', stopPropagation: 'isPropagationStopped' } function compatible(event, source) { if (source || !event.isDefaultPrevented) { source || (source = event) $.each(eventMethods, function(name, predicate) { var sourceMethod = source[name] event[name] = function(){ this[predicate] = returnTrue return sourceMethod && sourceMethod.apply(source, arguments) } event[predicate] = returnFalse }) try { event.timeStamp || (event.timeStamp = Date.now()) } catch (ignored) { } if (source.defaultPrevented !== undefined ? source.defaultPrevented : 'returnValue' in source ? source.returnValue === false : source.getPreventDefault && source.getPreventDefault()) event.isDefaultPrevented = returnTrue } return event } function createProxy(event) { var key, proxy = { originalEvent: event } for (key in event) if (!ignoreProperties.test(key) && event[key] !== undefined) proxy[key] = event[key] return compatible(proxy, event) } $.fn.delegate = function(selector, event, callback){ return this.on(event, selector, callback) } $.fn.undelegate = function(selector, event, callback){ return this.off(event, selector, callback) } $.fn.live = function(event, callback){ $(document.body).delegate(this.selector, event, callback) return this } $.fn.die = function(event, callback){ $(document.body).undelegate(this.selector, event, callback) return this } $.fn.on = function(event, selector, data, callback, one){ var autoRemove, delegator, $this = this if (event && !isString(event)) { $.each(event, function(type, fn){ $this.on(type, selector, data, fn, one) }) return $this } if (!isString(selector) && !isFunction(callback) && callback !== false) callback = data, data = selector, selector = undefined if (callback === undefined || data === false) callback = data, data = undefined if (callback === false) callback = returnFalse return $this.each(function(_, element){ if (one) autoRemove = function(e){ remove(element, e.type, callback) return callback.apply(this, arguments) } if (selector) delegator = function(e){ var evt, match = $(e.target).closest(selector, element).get(0) if (match && match !== element) { evt = $.extend(createProxy(e), {currentTarget: match, liveFired: element}) return (autoRemove || callback).apply(match, [evt].concat(slice.call(arguments, 1))) } } add(element, event, callback, data, selector, delegator || autoRemove) }) } $.fn.off = function(event, selector, callback){ var $this = this if (event && !isString(event)) { $.each(event, function(type, fn){ $this.off(type, selector, fn) }) return $this } if (!isString(selector) && !isFunction(callback) && callback !== false) callback = selector, selector = undefined if (callback === false) callback = returnFalse return $this.each(function(){ remove(this, event, callback, selector) }) } $.fn.trigger = function(event, args){ event = (isString(event) || $.isPlainObject(event)) ? $.Event(event) : compatible(event) event._args = args return this.each(function(){ // handle focus(), blur() by calling them directly if (event.type in focus && typeof this[event.type] == "function") this[event.type]() // items in the collection might not be DOM elements else if ('dispatchEvent' in this) this.dispatchEvent(event) else $(this).triggerHandler(event, args) }) } // triggers event handlers on current element just as if an event occurred, // doesn't trigger an actual event, doesn't bubble $.fn.triggerHandler = function(event, args){ var e, result this.each(function(i, element){ e = createProxy(isString(event) ? $.Event(event) : event) e._args = args e.target = element $.each(findHandlers(element, event.type || event), function(i, handler){ result = handler.proxy(e) if (e.isImmediatePropagationStopped()) return false }) }) return result } // shortcut methods for `.bind(event, fn)` for each event type ;('focusin focusout focus blur load resize scroll unload click dblclick '+ 'mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave '+ 'change select keydown keypress keyup error').split(' ').forEach(function(event) { $.fn[event] = function(callback) { return (0 in arguments) ? this.bind(event, callback) : this.trigger(event) } }) $.Event = function(type, props) { if (!isString(type)) props = type, type = props.type var event = document.createEvent(specialEvents[type] || 'Events'), bubbles = true if (props) for (var name in props) (name == 'bubbles') ? (bubbles = !!props[name]) : (event[name] = props[name]) event.initEvent(type, bubbles, true) return compatible(event) } })(Zepto) ;(function($){ var cache = [], timeout $.fn.remove = function(){ return this.each(function(){ if(this.parentNode){ if(this.tagName === 'IMG'){ cache.push(this) this.src = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=' if (timeout) clearTimeout(timeout) timeout = setTimeout(function(){ cache = [] }, 60000) } this.parentNode.removeChild(this) } }) } })(Zepto) ;(function($){ var data = {}, dataAttr = $.fn.data, camelize = $.camelCase, exp = $.expando = 'Zepto' + (+new Date()), emptyArray = [] // Get value from node: // 1. first try key as given, // 2. then try camelized key, // 3. fall back to reading "data-*" attribute. function getData(node, name) { var id = node[exp], store = id && data[id] if (name === undefined) return store || setData(node) else { if (store) { if (name in store) return store[name] var camelName = camelize(name) if (camelName in store) return store[camelName] } return dataAttr.call($(node), name) } } // Store value under camelized key on node function setData(node, name, value) { var id = node[exp] || (node[exp] = ++$.uuid), store = data[id] || (data[id] = attributeData(node)) if (name !== undefined) store[camelize(name)] = value return store } // Read all "data-*" attributes from a node function attributeData(node) { var store = {} $.each(node.attributes || emptyArray, function(i, attr){ if (attr.name.indexOf('data-') == 0) store[camelize(attr.name.replace('data-', ''))] = $.zepto.deserializeValue(attr.value) }) return store } $.fn.data = function(name, value) { return value === undefined ? // set multiple values via object $.isPlainObject(name) ? this.each(function(i, node){ $.each(name, function(key, value){ setData(node, key, value) }) }) : // get value from first element (0 in this ? getData(this[0], name) : undefined) : // set value on all elements this.each(function(){ setData(this, name, value) }) } $.data = function(elem, name, value) { return $(elem).data(name, value) } $.hasData = function(elem) { var id = elem[exp], store = id && data[id] return store ? !$.isEmptyObject(store) : false } $.fn.removeData = function(names) { if (typeof names == 'string') names = names.split(/\s+/) return this.each(function(){ var id = this[exp], store = id && data[id] if (store) $.each(names || store, function(key){ delete store[names ? camelize(this) : key] }) }) } // Generate extended `remove` and `empty` functions ;['remove', 'empty'].forEach(function(methodName){ var origFn = $.fn[methodName] $.fn[methodName] = function() { var elements = this.find('*') if (methodName === 'remove') elements = elements.add(this) elements.removeData() return origFn.call(this) } }) })(Zepto) return Zepto })) ================================================ FILE: js/instantsearch.js/CHANGELOG.md ================================================ ## [4.49.1](https://github.com/algolia/instantsearch.js/compare/v4.49.0...v4.49.1) (2022-11-01) ### Bug Fixes * **insights:** check before usage of `document` ([#5149](https://github.com/algolia/instantsearch.js/issues/5149)) ([6733dea](https://github.com/algolia/instantsearch.js/commit/6733dea1091a3a6c8ec9049eba652a7f06e9c501)) # [4.49.0](https://github.com/algolia/instantsearch.js/compare/v4.48.1...v4.49.0) (2022-10-25) ### Features * **poweredBy:** update component logo ([#5145](https://github.com/algolia/instantsearch.js/issues/5145)) ([7df7816](https://github.com/algolia/instantsearch.js/commit/7df7816eac1bb3d2eafee5da7b6f4f59611468b2)) ## [4.48.1](https://github.com/algolia/instantsearch.js/compare/v4.48.0...v4.48.1) (2022-10-18) ### Bug Fixes * **bundlesize:** consolidate usage of "classnames" helper ([#5138](https://github.com/algolia/instantsearch.js/issues/5138)) ([f1ec288](https://github.com/algolia/instantsearch.js/commit/f1ec28889be5c2f906dd398f37d072587e29cf3a)) * **currentRefinements:** reset page number on refine ([#5136](https://github.com/algolia/instantsearch.js/issues/5136)) ([407b576](https://github.com/algolia/instantsearch.js/commit/407b5767b51c26d5f471071a92f2e32762898f24)) * **events:** prevent warning on low number of listeners ([#5143](https://github.com/algolia/instantsearch.js/issues/5143)) ([432aa70](https://github.com/algolia/instantsearch.js/commit/432aa7006e7d8eefd1c8c382f59ea2d2974a19da)) # [4.48.0](https://github.com/algolia/instantsearch.js/compare/v4.47.0...v4.48.0) (2022-10-10) ### Bug Fixes * **insightsMiddleware:** infer type of insightsClient for onEvent ([#5130](https://github.com/algolia/instantsearch.js/issues/5130)) ([dd5fca4](https://github.com/algolia/instantsearch.js/commit/dd5fca4c185c66f1e31ebe9c0568bcad48e062f3)), closes [#5129](https://github.com/algolia/instantsearch.js/issues/5129) ### Features * **routing:** include repeated indexId in URL correctly ([#5134](https://github.com/algolia/instantsearch.js/issues/5134)) ([679f5da](https://github.com/algolia/instantsearch.js/commit/679f5dad839536def6ae9c3a18416296d40ed49a)) # [4.47.0](https://github.com/algolia/instantsearch.js/compare/v4.46.3...v4.47.0) (2022-10-03) ### Bug Fixes * **hierarchicalMenu:** pass correct attribute name to Insights ([#5124](https://github.com/algolia/instantsearch.js/issues/5124)) ([fe18a16](https://github.com/algolia/instantsearch.js/commit/fe18a168b1b195d067298770b55fd29a7fdb6edb)) ### Features * **status:** introduce status in InstantSearch class ([#5115](https://github.com/algolia/instantsearch.js/issues/5115)) ([21f3147](https://github.com/algolia/instantsearch.js/commit/21f31476e75e162b38b002d5439f231f3990e785)) * **hierarchicalMenu**: introduce `ais-HierarchicalMenu-item--selected` class ([#5125](https://github.com/algolia/instantsearch.js/issues/5125)) ([4ebb828](https://github.com/algolia/instantsearch.js/commit/4ebb828c93afabfd8083246dfe7edfd33932d5fd)) ## [4.46.3](https://github.com/algolia/instantsearch.js/compare/v4.46.2...v4.46.3) (2022-09-27) ### Bug Fixes * **currentRefinements:** implement noRefinementRoot modifier class ([#5114](https://github.com/algolia/instantsearch.js/issues/5114)) ([cb66830](https://github.com/algolia/instantsearch.js/commit/cb668305af26bf919841c25bd4cc8493fcdf8cf9)) ## [4.46.2](https://github.com/algolia/instantsearch.js/compare/v4.46.1...v4.46.2) (2022-09-22) ### Bug Fixes * **build:** remove jsx pragma comments from build output ([#5112](https://github.com/algolia/instantsearch.js/issues/5112)) ([6582083](https://github.com/algolia/instantsearch.js/commit/65820831b7d7e14867f13a2947795491730b8442)) * **imports:** split out templating from ./utils ([#5111](https://github.com/algolia/instantsearch.js/issues/5111)) ([fc765f3](https://github.com/algolia/instantsearch.js/commit/fc765f35ddd85068237edc81c66932b098e3b55a)), closes [#5109](https://github.com/algolia/instantsearch.js/issues/5109) ## [4.46.1](https://github.com/algolia/instantsearch.js/compare/v4.46.0...v4.46.1) (2022-09-15) ### Bug Fixes * **hierarchicalMenu:** use existing facet filters in multi queries for parent facet values ([#5105](https://github.com/algolia/instantsearch.js/issues/5105)) ([10a83f1](https://github.com/algolia/instantsearch.js/commit/10a83f146f714d9f97bb8edca2499f16df4ca22d)) * **insights:** make sure change in userToken can't reset the search parameters ([#5101](https://github.com/algolia/instantsearch.js/issues/5101)) ([b20c8dc](https://github.com/algolia/instantsearch.js/commit/b20c8dc70e34c1f234dc10eb7fc69296f30986a4)) * **setUiState**: call onStateChange handler ([#5104](https://github.com/algolia/instantsearch.js/issues/5104)) ([231853d](https://github.com/algolia/instantsearch.js/commit/231853dab731189a33ee480cdb196789c7336fda))) ## [4.46.0](https://github.com/algolia/instantsearch.js/compare/v4.45.1...v4.46.0) (2022-09-12) ### Features * **html:** deprecate Hogan.js and string-based templates ([#5095](https://github.com/algolia/instantsearch.js/issues/5095)) ([a06ddf5](https://github.com/algolia/instantsearch.js/commit/a06ddf55f1ffd1a93cddab2fcf95d2be3220a423)) * **html:** introduce `html` templating ([#5081](https://github.com/algolia/instantsearch.js/issues/5081)) ([e55e224](https://github.com/algolia/instantsearch.js/commit/e55e2245256193d27f2c85f24b8aab7c9048c554)) ## [4.45.1](https://github.com/algolia/instantsearch.js/compare/v4.45.0...v4.45.1) (2022-09-06) ### Bug Fixes * **ratingMenu:** fix `undefined` facet values error when `disjunctiveFacets` is empty ([#5096](https://github.com/algolia/instantsearch.js/issues/5096)) ([dd870d5](https://github.com/algolia/instantsearch.js/commit/dd870d5a658ce42b068eadf34f9b69772291aa20)) # [4.45.0](https://github.com/algolia/instantsearch.js/compare/v4.44.1...v4.45.0) (2022-08-29) ### Features * **connectors:** deprecate `hasNoResults` in favor of `canRefine` ([#5091](https://github.com/algolia/instantsearch.js/issues/5091)) ([1749a4e](https://github.com/algolia/instantsearch.js/commit/1749a4eb9a2f28fa4a8d442163e3b10acbde7c22)) ## [4.44.1](https://github.com/algolia/instantsearch.js/compare/v4.44.0...v4.44.1) (2022-08-25) ### Bug Fixes * **connectNumericMenu + connectRange:** stop sending invalid clickedFilters event ([#5085](https://github.com/algolia/instantsearch.js/issues/5085)) ([20996c7](https://github.com/algolia/instantsearch.js/commit/20996c7a159988c58e00ff24d2d2dc98af8b980f)) # [4.44.0](https://github.com/algolia/instantsearch.js/compare/v4.43.1...v4.44.0) (2022-08-08) ### Features * **geo-search:** make `GeoHit` type generic ([#5083](https://github.com/algolia/instantsearch.js/issues/5083)) ([3d3c7b2](https://github.com/algolia/instantsearch.js/commit/3d3c7b298b74effe9bb722a04fbb47dc39a4bd95)) ## [4.43.1](https://github.com/algolia/instantsearch.js/compare/v4.43.0...v4.43.1) (2022-07-11) ### Bug Fixes * **errors:** rethrow error as error if it's an object ([#5075](https://github.com/algolia/instantsearch.js/issues/5075)) ([34132bb](https://github.com/algolia/instantsearch.js/commit/34132bba38c05fa2f5e4e54c6889e9335e62e4f4)) * **ratingMenu:** don't warn if results are artificial ([#5073](https://github.com/algolia/instantsearch.js/issues/5073)) ([d747d23](https://github.com/algolia/instantsearch.js/commit/d747d23b28c380fe82a40eeab06c57359af8004a)) * **types:** use correct case for _geoloc property ([#5074](https://github.com/algolia/instantsearch.js/issues/5074)) ([6fed7d8](https://github.com/algolia/instantsearch.js/commit/6fed7d870c3607980776d33a3697f8e2789aa08b)) # [4.43.0](https://github.com/algolia/instantsearch.js/compare/v4.42.0...v4.43.0) (2022-06-28) ### Features * **types:** support algoliasearch v5 ([#5066](https://github.com/algolia/instantsearch.js/issues/5066)) ([3eb4dc7](https://github.com/algolia/instantsearch.js/commit/3eb4dc75a5935f2ee4fead8787f39af0150b24c4)) # [4.42.0](https://github.com/algolia/instantsearch.js/compare/v4.41.2...v4.42.0) (2022-06-21) ### Bug Fixes * **es:** update import path for `infiniteHitsCache` in depreciation message ([#5068](https://github.com/algolia/instantsearch.js/issues/5068)) ([545cbaf](https://github.com/algolia/instantsearch.js/commit/545cbafd748bb8be32bff66ac60b5f3f9133a5b4)) ### Features * **core:** sort parameters & support client.search for sffv ([#5069](https://github.com/algolia/instantsearch.js/issues/5069)) ([34e2b00](https://github.com/algolia/instantsearch.js/commit/34e2b00cbc93f1bc86ee0abaec6b6e132bd18354)) ## [4.41.2](https://github.com/algolia/instantsearch.js/compare/v4.41.1...v4.41.2) (2022-06-15) ### Bug Fixes * **hierarchicalMenu:** show full hierarchical parent values ([#5063](https://github.com/algolia/instantsearch.js/issues/5063)) ([cd1db34](https://github.com/algolia/instantsearch.js/commit/cd1db34815f92acb3d2d0cec6c1ae7865d14fb13)) ## [4.41.1](https://github.com/algolia/instantsearch.js/compare/v4.41.0...v4.41.1) (2022-06-14) ### Bug Fixes * **insights:** don't send view event if search is stalled ([#5058](https://github.com/algolia/instantsearch.js/issues/5058)) ([1686dfb](https://github.com/algolia/instantsearch.js/commit/1686dfb096cfce062e268feda7956e3b160bf2da)), closes [/github.com/algolia/instantsearch.js/blob/99f6fe1dc51e4815e5b9efcfb30e3e2f3127e763/src/lib/utils/createSendEventForHits.ts#L168](https://github.com//github.com/algolia/instantsearch.js/blob/99f6fe1dc51e4815e5b9efcfb30e3e2f3127e763/src/lib/utils/createSendEventForHits.ts/issues/L168) [/github.com/algolia/instantsearch.js/blob/55313e4ea4105b777f3f102e9f48a7e440496d25/src/middlewares/createInsightsMiddleware.ts#L144](https://github.com//github.com/algolia/instantsearch.js/blob/55313e4ea4105b777f3f102e9f48a7e440496d25/src/middlewares/createInsightsMiddleware.ts/issues/L144) * **types:** avoid inferring UiState type from initialUiState ([#5061](https://github.com/algolia/instantsearch.js/issues/5061)) ([80ca07e](https://github.com/algolia/instantsearch.js/commit/80ca07e29064357343ee997be94ef10beadba637)), closes [/github.com/Microsoft/TypeScript/issues/14829#issuecomment-504042546](https://github.com//github.com/Microsoft/TypeScript/issues/14829/issues/issuecomment-504042546) [#5060](https://github.com/algolia/instantsearch.js/issues/5060) * **types:** make all usages of UiState in InstantSearch generic ([#5060](https://github.com/algolia/instantsearch.js/issues/5060)) ([2b9e76b](https://github.com/algolia/instantsearch.js/commit/2b9e76b568fb4d4cc5bd49c384ee583d84d6f39a)) # [4.41.0](https://github.com/algolia/instantsearch.js/compare/v4.40.6...v4.41.0) (2022-06-01) ### Features * **core:** don't schedule search without widgets ([#5056](https://github.com/algolia/instantsearch.js/issues/5056)) ([ea3d6d9](https://github.com/algolia/instantsearch.js/commit/ea3d6d9c6ae1fe2f90bf5643d4bdcbb89507e9bc)) ## [4.40.6](https://github.com/algolia/instantsearch.js/compare/v4.40.5...v4.40.6) (2022-05-24) ### Bug Fixes * **types:** only allow `null` for parent in `getWidgetRenderState` if widget is an index ([#5052](https://github.com/algolia/instantsearch.js/issues/5052)) ([fe0fce0](https://github.com/algolia/instantsearch.js/commit/fe0fce0641ffff9af1d1303b7ee71d77ba08f8bd)) ## [4.40.5](https://github.com/algolia/instantsearch.js/compare/v4.40.4...v4.40.5) (2022-04-26) ### Bug Fixes * **routing:** prevent writing the same URL twice ([#5045](https://github.com/algolia/instantsearch.js/issues/5045)) ([5d79d92](https://github.com/algolia/instantsearch.js/commit/5d79d92b30e188e5206dcb5fe86fcac058c3f09b)) ## [4.40.4](https://github.com/algolia/instantsearch.js/compare/v4.40.3...v4.40.4) (2022-04-13) ### Bug Fixes * **currentRefinements:** correctly show and allow for refining escaped values ([#5041](https://github.com/algolia/instantsearch.js/issues/5041)) ([277f4df](https://github.com/algolia/instantsearch.js/commit/277f4dff21fb7eeaeb41a8c49aaaf707f880ee58)) ## [4.40.3](https://github.com/algolia/instantsearch.js/compare/v4.40.2...v4.40.3) (2022-04-04) ### Bug Fixes * **refinements:** escape facet values starting with "-" ([#5039](https://github.com/algolia/instantsearch.js/issues/5039)) ([6b6f4e8](https://github.com/algolia/instantsearch.js/commit/6b6f4e86550a3c9dd02f3a8400d832cef64cb45d)) ## [4.40.2](https://github.com/algolia/instantsearch.js/compare/v4.40.1...v4.40.2) (2022-03-29) ### Bug Fixes * **currentRefinements:** more detailed type for item.type ([#5034](https://github.com/algolia/instantsearch.js/issues/5034)) ([773e2c6](https://github.com/algolia/instantsearch.js/commit/773e2c65840f86881eb3dd8825c8c4ad9c73aec9)) ## [4.40.1](https://github.com/algolia/instantsearch.js/compare/v4.40.0...v4.40.1) (2022-03-21) ### Bug Fixes * **types:** update to latest algoliasearch-helper ([6bbe790](https://github.com/algolia/instantsearch.js/commit/6bbe790a99320b4237b81614472c048ffe4426d8)) # [4.40.0](https://github.com/algolia/instantsearch.js/compare/v4.39.2...v4.40.0) (2022-03-21) ### Features * **infiniteHits:** avoid caching artificial results ([#5023](https://github.com/algolia/instantsearch.js/issues/5023)) ([e8c0145](https://github.com/algolia/instantsearch.js/commit/e8c01452ebe77b82b8a107c5d4fc026abf5645d8)) ## [4.39.2](https://github.com/algolia/instantsearch.js/compare/v4.39.1...v4.39.2) (2022-03-14) ### Bug Fixes * fix types of `sortBy` option ([#5024](https://github.com/algolia/instantsearch.js/issues/5024)) ([3f7ea32](https://github.com/algolia/instantsearch.js/commit/3f7ea32374e0e409ebf27b07d28cf3871a5b33b3)) ## [4.39.1](https://github.com/algolia/instantsearch.js/compare/v4.39.0...v4.39.1) (2022-03-01) ### Bug Fixes * **insights:** send view events after rendering ([#5014](https://github.com/algolia/instantsearch.js/issues/5014)) ([e952abc](https://github.com/algolia/instantsearch.js/commit/e952abc64043a55e06c9c46a656bc98ad45d1502)) # [4.39.0](https://github.com/algolia/instantsearch.js/compare/v4.38.1...v4.39.0) (2022-02-23) ### Features * **ts:** allow Hits related connectors to be generic ([#5019](https://github.com/algolia/instantsearch.js/issues/5019)) ([e986f7e](https://github.com/algolia/instantsearch.js/commit/e986f7e46d57173da4d3d6c3c23fbdf3f9c0f78c)) ## [4.38.1](https://github.com/algolia/instantsearch.js/compare/v4.38.0...v4.38.1) (2022-02-08) ### Bug Fixes * **routing:** fix history router based on history length ([#5004](https://github.com/algolia/instantsearch.js/issues/5004)) ([40541af](https://github.com/algolia/instantsearch.js/commit/40541af5c8face0e32a1ec3a4665a8387d89c626)) * **metadata:** ensure safe user agent detection ([#5009](https://github.com/algolia/instantsearch.js/pull/5009) [15a6a9d](https://github.com/algolia/instantsearch.js/commit/15a6a9d10ee512fab6884696bc59bedea13bd1b3)) # [4.38.0](https://github.com/algolia/instantsearch.js/compare/v4.37.3...v4.38.0) (2022-01-28) ### Bug Fixes * **typescript:** remove non-existing UMD type definition ([#5001](https://github.com/algolia/instantsearch.js/issues/5001)) ([c234374](https://github.com/algolia/instantsearch.js/commit/c234374a1f5333f6625980c45fa0833a8c130257)) ### Features * **connectors:** expose search results to `transformItems` when available ([#5000](https://github.com/algolia/instantsearch.js/issues/5000)) ([58c2651](https://github.com/algolia/instantsearch.js/commit/58c26517aad916ce49b474458e3411ff7ef5497a)) ## [4.37.3](https://github.com/algolia/instantsearch.js/compare/v4.37.2...v4.37.3) (2022-01-25) ### Bug Fixes * **helpers:** display warning if attribute cannot be highlighted/snippeted ([#4996](https://github.com/algolia/instantsearch.js/issues/4996)) ([e81bf59](https://github.com/algolia/instantsearch.js/commit/e81bf59f0f28eb7b9f54f7d4424c60546b9a4d8c)) ## [4.37.2](https://github.com/algolia/instantsearch.js/compare/v4.37.1...v4.37.2) (2022-01-10) ### Bug Fixes * **searchbox:** make sure setting query to the initial doesn't cause a stale state ([#4990](https://github.com/algolia/instantsearch.js/issues/4990)) ([3faca01](https://github.com/algolia/instantsearch.js/commit/3faca014aad08145c3b4cc66a5e841da3a0f64b8)) ## [4.37.1](https://github.com/algolia/instantsearch.js/compare/v4.37.0...v4.37.1) (2022-01-05) ### Bug Fixes * **connectBreadcrumb:** returns an empty array if no hierarchicalFacets exist ([#4980](https://github.com/algolia/instantsearch.js/issues/4980)) ([3ea9b91](https://github.com/algolia/instantsearch.js/commit/3ea9b918f85c686a07b06cfc12b8c59b80181f28)) * **es:** mark inner package.json as side-effect free ([#4984](https://github.com/algolia/instantsearch.js/issues/4984)) ([74f56f3](https://github.com/algolia/instantsearch.js/commit/74f56f35b7ccc78904592edfc40e782e40847986)), closes [#4971](https://github.com/algolia/instantsearch.js/issues/4971) * **events:** emit error as typeof Error ([#4983](https://github.com/algolia/instantsearch.js/issues/4983)) ([4adfaf2](https://github.com/algolia/instantsearch.js/commit/4adfaf2eba40fffa7f4800664dc89e0edf2d819e)) # [4.37.0](https://github.com/algolia/instantsearch.js/compare/v4.36.0...v4.37.0) (2022-01-04) ### Features * **build:** expose `/es` as a real ES module ([#4971](https://github.com/algolia/instantsearch.js/issues/4971)) ([e5b3434](https://github.com/algolia/instantsearch.js/commit/e5b343490921f70736e11a7758bdc7a3aeed6d69)) # [4.36.0](https://github.com/algolia/instantsearch.js/compare/v4.35.0...v4.36.0) (2021-12-16) ### Features * **dynamicWidgets:** send facets * and maxValuesPerFacet by default ([#4968](https://github.com/algolia/instantsearch.js/issues/4968)) ([969ae89](https://github.com/algolia/instantsearch.js/commit/969ae8980f7c8a055bb4c6c5967d04744644f555)) * **DynamicWidgets:** throw when transformItems returns anything that isn't an array ([#4975](https://github.com/algolia/instantsearch.js/issues/4975)) ([5c328c8](https://github.com/algolia/instantsearch.js/commit/5c328c85428eb9a5c1450fd01154751f4e0ea2fa)) # [4.35.0](https://github.com/algolia/instantsearch.js/compare/v4.34.0...v4.35.0) (2021-12-13) ### Features * **events:** move to @algolia/events ([#4961](https://github.com/algolia/instantsearch.js/issues/4961)) ([1c56726](https://github.com/algolia/instantsearch.js/commit/1c5672640c65d7ed6f6e381a3162e508bdda44f3)) ### Bug Fixes * **deps:** Add missing peer dependency ([#4950](https://github.com/algolia/instantsearch.js/issues/4950)) ([468578da9](https://github.com/algolia/instantsearch.js/commit/468578da948a12224c892fd12cba4c880aa7b25f)) # [4.34.0](https://github.com/algolia/instantsearch.js/compare/v4.33.2...v4.34.0) (2021-12-07) ### Features * rely on `state` in `getWidgetRenderState` ([#4960](https://github.com/algolia/instantsearch.js/issues/4960)) ([5006841](https://github.com/algolia/instantsearch.js/commit/50068417e5e7211802bc717b582946f6e630d7ac)) * support initial results (experimental) ([#4967](https://github.com/algolia/instantsearch.js/issues/4967)) ([db11c13](https://github.com/algolia/instantsearch.js/commit/db11c13ea55433491f5e924633bff12a303c1bc6)) ## [4.33.2](https://github.com/algolia/instantsearch.js/compare/v4.33.1...v4.33.2) (2021-11-16) ### Bug Fixes * **connectNumericMenu:** allow option for same start/end values ([#4951](https://github.com/algolia/instantsearch.js/issues/4951)) ([18da714](https://github.com/algolia/instantsearch.js/commit/18da714574fa98957d29014add3123e9c377551f)) ## [4.33.1](https://github.com/algolia/instantsearch.js/compare/v4.33.0...v4.33.1) (2021-11-02) ### Bug Fixes * **getUiState:** support `initialUiState` ([#4948](https://github.com/algolia/instantsearch.js/issues/4948)) ([532474d](https://github.com/algolia/instantsearch.js/commit/532474dfaf49446ab59a2a27424ca220947dd5bd)) # [4.33.0](https://github.com/algolia/instantsearch.js/compare/v4.32.0...v4.33.0) (2021-10-26) ### Bug Fixes * **router:** skip history push on browser back and forward actions ([#4933](https://github.com/algolia/instantsearch.js/issues/4933)) ([7909da4](https://github.com/algolia/instantsearch.js/commit/7909da4903eb1aee885811e280b909a3bda488be)) * **setUiState:** reset UI state with empty object ([#4944](https://github.com/algolia/instantsearch.js/issues/4944)) ([5faae4a](https://github.com/algolia/instantsearch.js/commit/5faae4ac44ac5ad2f8086ad2a306bcfaa14bc754)) ### Features * **router:** support server environments ([#4940](https://github.com/algolia/instantsearch.js/issues/4940)) ([a002962](https://github.com/algolia/instantsearch.js/commit/a002962df0e7683b29bef8bfaaddb494fa551a14)) # [4.32.0](https://github.com/algolia/instantsearch.js/compare/v4.31.1...v4.32.0) (2021-10-20) ### Features * **dependencies:** update algoliasearch-helper ([#4936](https://github.com/algolia/instantsearch.js/issues/4936)) ([014a413](https://github.com/algolia/instantsearch.js/commit/014a413f14dded3861a9c288ea618f1602bcd66d)) ## [4.31.1](https://github.com/algolia/instantsearch.js/compare/v4.31.0...v4.31.1) (2021-10-19) ### Bug Fixes * **types:** export correct types from search-insights ([#4930](https://github.com/algolia/instantsearch.js/issues/4930)) ([5ae7a5b](https://github.com/algolia/instantsearch.js/commit/5ae7a5b86ad9c042bfbdc60e505c159eebdb404f)) # [4.31.0](https://github.com/algolia/instantsearch.js/compare/v4.30.3...v4.31.0) (2021-10-14) ### Features * **InstantSearch:** defer initial search ([#4925](https://github.com/algolia/instantsearch.js/issues/4925)) ([9a88115](https://github.com/algolia/instantsearch.js/commit/9a8811534af1288e316cdfb6f6fc49df1597290e)) ## [4.30.3](https://github.com/algolia/instantsearch.js/compare/v4.30.2...v4.30.3) (2021-10-12) ### Bug Fixes * **toggleRefinement:** don't set off value in getWidgetRenderState ([#4912](https://github.com/algolia/instantsearch.js/issues/4912)) ([69525bf](https://github.com/algolia/instantsearch.js/commit/69525bf2a3087aeb75c4f1e5ab8452012436f61f)) ## [4.30.2](https://github.com/algolia/instantsearch.js/compare/v4.30.1...v4.30.2) (2021-09-21) ### Bug Fixes * **es:** add warning to typescript declaration of keys to be imported from helpers ([#4908](https://github.com/algolia/instantsearch.js/issues/4908)) ([8cbd5fb](https://github.com/algolia/instantsearch.js/commit/8cbd5fb3f02427f2c7de6e818f1ff4c81485b3e1)) * **infinite/hits:** stop saving the transformed results in cache ([#4907](https://github.com/algolia/instantsearch.js/issues/4907)) ([82dc0ae](https://github.com/algolia/instantsearch.js/commit/82dc0ae966fda37582d5324ea6ca3e0f33ef56d5)), closes [#4819](https://github.com/algolia/instantsearch.js/issues/4819) ## [4.30.1](https://github.com/algolia/instantsearch.js/compare/v4.30.0...v4.30.1) (2021-09-14) ### Bug Fixes * **insightsMiddleware:** throw an error when credentials can't be extracted ([#4901](https://github.com/algolia/instantsearch.js/issues/4901)) ([55313e4](https://github.com/algolia/instantsearch.js/commit/55313e4ea4105b777f3f102e9f48a7e440496d25)) # [4.30.0](https://github.com/algolia/instantsearch.js/compare/v4.29.1...v4.30.0) (2021-09-07) ### Bug Fixes * **insights:** handle multiple setUserToken call before search.start() ([#4897](https://github.com/algolia/instantsearch.js/issues/4897)) ([51a6f2b](https://github.com/algolia/instantsearch.js/commit/51a6f2bcd2ea312e7038e6f3208a2e9b3fed494a)) ### Features * **dynamicWidgets:** add fallbackWidget ([#4847](https://github.com/algolia/instantsearch.js/issues/4847)) ([7d99ab9](https://github.com/algolia/instantsearch.js/commit/7d99ab95972d5886cdc82abb5794a41d38381a50)) * **dynamicWidgets:** mark as stable ([#4899](https://github.com/algolia/instantsearch.js/issues/4899)) ([f97468f](https://github.com/algolia/instantsearch.js/commit/f97468f134d92c198433a7dad16a3b19b3779a94)) ## [4.29.1](https://github.com/algolia/instantsearch.js/compare/v4.29.0...v4.29.1) (2021-09-02) ### Bug Fixes * **middleware:** subscribe middleware before initializing main index ([#4849](https://github.com/algolia/instantsearch.js/issues/4849)) ([0fc8f73](https://github.com/algolia/instantsearch.js/commit/0fc8f7322f8521f934ed871e8125707ba2ec0bfd)) # [4.29.0](https://github.com/algolia/instantsearch.js/compare/v4.28.0...v4.29.0) (2021-08-31) ### Features * **panel:** render templates on init with render state ([#4845](https://github.com/algolia/instantsearch.js/issues/4845)) ([0e151a9](https://github.com/algolia/instantsearch.js/commit/0e151a9552092807ecbc6993f3f6193fef621f44)) # [4.28.0](https://github.com/algolia/instantsearch.js/compare/v4.27.2...v4.28.0) (2021-08-24) ### Bug Fixes * **sendEvent:** split > 20 objects in multiple calls ([#4841](https://github.com/algolia/instantsearch.js/issues/4841)) ([44574bc](https://github.com/algolia/instantsearch.js/commit/44574bcf03ac05e22274099622e6a1839599ca7e)) * **svg:** remove xmlns ([#4839](https://github.com/algolia/instantsearch.js/issues/4839)) ([932ae3a](https://github.com/algolia/instantsearch.js/commit/932ae3a868340a32ccaacb276c862921fee41a93)) ### Features * **ts:** expose built files in umd ([#4844](https://github.com/algolia/instantsearch.js/issues/4844)) ([8578ae3](https://github.com/algolia/instantsearch.js/commit/8578ae30a915db49acaa0292faba2ec6ccd52b73)) ## [4.27.2](https://github.com/algolia/instantsearch.js/compare/v4.27.1...v4.27.2) (2021-08-18) ### Bug Fixes * **types:** export all types as "type" to avoid exporting in .js ([#4837](https://github.com/algolia/instantsearch.js/issues/4837)) ([dcbbd88](https://github.com/algolia/instantsearch.js/commit/dcbbd8804b4b6471d24820b42826b57388974c27)) ## [4.27.1](https://github.com/algolia/instantsearch.js/compare/v4.27.0...v4.27.1) (2021-08-17) ### Bug Fixes * **ts:** export types from entry point ([#4834](https://github.com/algolia/instantsearch.js/issues/4834)) ([3014e84](https://github.com/algolia/instantsearch.js/commit/3014e8481e401db62fff41d6867580c04adeaf6b)) # [4.27.0](https://github.com/algolia/instantsearch.js/compare/v4.26.0...v4.27.0) (2021-08-17) ### Bug Fixes * **ts:** correct entry point ([#4829](https://github.com/algolia/instantsearch.js/issues/4829)) ([24a45f9](https://github.com/algolia/instantsearch.js/commit/24a45f9a9fb3c8f62003d2aa37b3456c11af2985)) * **ts:** export PaginationConnector ([d201322](https://github.com/algolia/instantsearch.js/commit/d201322de0d09a664b762422fdc0a51e2bd566bc)) ### Features * **typescript:** expose types at regular build ([#4832](https://github.com/algolia/instantsearch.js/issues/4832)) ([4bea07b](https://github.com/algolia/instantsearch.js/commit/4bea07b99f492441eb94e483378e0778f90c5b43)) If you were using typescript via the `experimental-typescript` tag, you can now use regular InstantSearch.js. # [4.26.0](https://github.com/algolia/instantsearch.js/compare/v4.25.3...v4.26.0) (2021-08-10) ### Features * **ts:** allow custom ui state and route state in routing ([#4816](https://github.com/algolia/instantsearch.js/issues/4816)) ([5f8ba5d](https://github.com/algolia/instantsearch.js/commit/5f8ba5ddcf5e32fd3cecf39ea667d8266dab35f8)) * **types:** allow typed access to properties added to entry ([#4814](https://github.com/algolia/instantsearch.js/issues/4814)) ([9000f16](https://github.com/algolia/instantsearch.js/commit/9000f16c3e0ff53eda4ca21281a87d8ff9b9154d)) ## [4.25.3](https://github.com/algolia/instantsearch.js/compare/v4.25.2...v4.25.3) (2021-08-03) ### Bug Fixes * **types:** fix hits and results types in connectHits and connectInfiniteHits ([#4820](https://github.com/algolia/instantsearch.js/issues/4820)) ([2bf987e](https://github.com/algolia/instantsearch.js/commit/2bf987e8b2728a8e65a88a49d46eadf6c0172660)) ## [4.25.2](https://github.com/algolia/instantsearch.js/compare/v4.25.1...v4.25.2) (2021-07-20) ### Bug Fixes * **build:** ensure build fails when types building fails ([#4812](https://github.com/algolia/instantsearch.js/issues/4812)) ([b37e23b](https://github.com/algolia/instantsearch.js/commit/b37e23b5819abbc03049124bc3a29120f91aeb8c)) * **types:** export widget's types ([#4813](https://github.com/algolia/instantsearch.js/issues/4813)) ([e9764e9](https://github.com/algolia/instantsearch.js/commit/e9764e9273e5b7bacd86f8d1cb751e87bd75eb75)) ## [4.25.1](https://github.com/algolia/instantsearch.js/compare/v4.25.0...v4.25.1) (2021-07-13) ### Bug Fixes * **deps:** force a lower version of qs ([#4805](https://github.com/algolia/instantsearch.js/issues/4805)) ([07b7e08](https://github.com/algolia/instantsearch.js/commit/07b7e086282f8cc6a17aee822902d97204c1d2da)) # [4.25.0](https://github.com/algolia/instantsearch.js/compare/v4.24.3...v4.25.0) (2021-07-06) ### Features * **facets:** apply result from facet ordering ([#4784](https://github.com/algolia/instantsearch.js/issues/4784)) ([9e9d839](https://github.com/algolia/instantsearch.js/commit/9e9d8394067bec35425b7d66f94fcce504faee7f)) ## [4.24.3](https://github.com/algolia/instantsearch.js/compare/v4.24.2...v4.24.3) (2021-07-05) ### Bug Fixes * **dynamicWidgets:** read from facetOrdering.facets ([42d6c6c](https://github.com/algolia/instantsearch.js/commit/42d6c6cefc5f009a3cfc63ab3d628ed2811f1700)) * **ts:** make template types consistent ([#4785](https://github.com/algolia/instantsearch.js/issues/4785)) ([e0fbd55](https://github.com/algolia/instantsearch.js/commit/e0fbd55b6b98dd64301f113fd394dce57552d94c)) ## [4.24.2](https://github.com/algolia/instantsearch.js/compare/v4.24.1...v4.24.2) (2021-06-29) ### Bug Fixes * **index:** export `IndexWidgetParams` type ([#4793](https://github.com/algolia/instantsearch.js/issues/4793)) ([91bdea1](https://github.com/algolia/instantsearch.js/commit/91bdea18f3768265937e2d3aca4acaa05c24e426)) * **onStateChange:** propagate change to middleware ([#4796](https://github.com/algolia/instantsearch.js/issues/4796)) ([57c32c0](https://github.com/algolia/instantsearch.js/commit/57c32c0a43bd2c6cbdd3f8ea7eac8109e3024f2a)) * **relevantSort:** export `RelevantSortWidgetParams` type ([#4794](https://github.com/algolia/instantsearch.js/issues/4794)) ([1a10b59](https://github.com/algolia/instantsearch.js/commit/1a10b59938c6121f58510726b67ee6dfa1aa1b7c)) * **sortBy:** do not write the default state ([#4798](https://github.com/algolia/instantsearch.js/issues/4798)) ([1d8a40e](https://github.com/algolia/instantsearch.js/commit/1d8a40ecc8e6e48746113ec3ec0d975e14bec1ea)) ## [4.24.1](https://github.com/algolia/instantsearch.js/compare/v4.24.0...v4.24.1) (2021-06-23) ### Bug Fixes * **mainHelper:** allow a mainHelper to be set before start ([#4790](https://github.com/algolia/instantsearch.js/issues/4790)) ([e8329ae](https://github.com/algolia/instantsearch.js/commit/e8329aecb386755a039cf10850e394d0d71f29f4)) # [4.24.0](https://github.com/algolia/instantsearch.js/compare/v4.23.0...v4.24.0) (2021-06-15) ### Bug Fixes * **clearRefinements:** do not throw when widgetParams is not given ([#4778](https://github.com/algolia/instantsearch.js/issues/4778)) ([6b1a375](https://github.com/algolia/instantsearch.js/commit/6b1a375ed7139c0b98993c0cb7ab40838e1f2288)) * **ts:** make `CSSClasses` types consistent ([#4774](https://github.com/algolia/instantsearch.js/issues/4774)) ([99008a9](https://github.com/algolia/instantsearch.js/commit/99008a985ddc61ce197200df51fdcf385914064d)) ### Features * **dynamicWidgets:** add default attributesToRender & transformItems ([#4776](https://github.com/algolia/instantsearch.js/issues/4776)) ([44dab44](https://github.com/algolia/instantsearch.js/commit/44dab44282da58b36a707ad80aff4c18477abccd)) * **ts:** convert pagination widget and component ([#4765](https://github.com/algolia/instantsearch.js/issues/4765)) ([34eb950](https://github.com/algolia/instantsearch.js/commit/34eb9500a2d7072814fd715e1c2217ed22de30d1)) * **ts:** convert rangeInput widget and component ([#4766](https://github.com/algolia/instantsearch.js/issues/4766)) ([40b1a82](https://github.com/algolia/instantsearch.js/commit/40b1a82f9df4b16708fceefbba77a8fb49c7dc41)) # [4.23.0](https://github.com/algolia/instantsearch.js/compare/v4.22.0...v4.23.0) (2021-05-25) ### Bug Fixes * **range:** reset the page on refine ([#4760](https://github.com/algolia/instantsearch.js/issues/4760)) ([24e3b34](https://github.com/algolia/instantsearch.js/commit/24e3b34c944ec32b414e845550e9c6c02b39cb92)), closes [#4759](https://github.com/algolia/instantsearch.js/issues/4759) ### Features * **ts:** convert poweredBy widget ([#4756](https://github.com/algolia/instantsearch.js/issues/4756)) ([142660a](https://github.com/algolia/instantsearch.js/commit/142660a2bc0ab7212265a9ff6dadf7a7f1081c69)) # [4.22.0](https://github.com/algolia/instantsearch.js/compare/v4.21.0...v4.22.0) (2021-05-05) ### Bug Fixes * **insights:** do not throw when userToken is not given ([#4724](https://github.com/algolia/instantsearch.js/issues/4724)) ([8241b29](https://github.com/algolia/instantsearch.js/commit/8241b2909c981a6bb52e9f4f9b6bacb7bc60263b)) * **insights:** use getUserToken method instead of _get ([#4744](https://github.com/algolia/instantsearch.js/issues/4744)) ([05d05a9](https://github.com/algolia/instantsearch.js/commit/05d05a9a8ad79e4ec8b183a3d17c2360430c302e)) * **relevantSort:** remove "relevantSort" nesting, since there's only one property ([#4735](https://github.com/algolia/instantsearch.js/issues/4735)) ([f742083](https://github.com/algolia/instantsearch.js/commit/f74208396159524086341be4acf84d2af2b44135)) * **connectToggleRefinement:** nest getRenderState per attribute ([#4743](https://github.com/algolia/instantsearch.js/issues/4743)) ([b9c884d](https://github.com/algolia/instantsearch.js/commit/b9c884daa406e1be63482ed198674b2ba22e66f2)) * **connectToggleRefinement:** remove search parameters from render state ([#4743](https://github.com/algolia/instantsearch.js/issues/4743)) ([b9c884d](https://github.com/algolia/instantsearch.js/commit/b9c884daa406e1be63482ed198674b2ba22e66f2)) ### Features * **core:** add getUiState function ([#4750](https://github.com/algolia/instantsearch.js/issues/4750)) ([adce212](https://github.com/algolia/instantsearch.js/commit/adce2127de6c652ee6364e889a525d9d0ff6efdd)) * **dynamicWidgets:** implementation ([#4687](https://github.com/algolia/instantsearch.js/issues/4687)) ([2e7ccc9](https://github.com/algolia/instantsearch.js/commit/2e7ccc91c8d2e4aa50c82a186cce057907042ed4)) * **ts:** migrate toggleRefinement & connectToggleRefinement ([#4743](https://github.com/algolia/instantsearch.js/issues/4743)) ([b9c884d](https://github.com/algolia/instantsearch.js/commit/b9c884daa406e1be63482ed198674b2ba22e66f2)) * **widget:** add access to "parent" in dispose ([#4745](https://github.com/algolia/instantsearch.js/issues/4745)) ([3fca986](https://github.com/algolia/instantsearch.js/commit/3fca986542e8b18312a6c6be810bf5fb986804a4)) # [4.21.0](https://github.com/algolia/instantsearch.js/compare/v4.20.0...v4.21.0) (2021-04-12) ### Bug Fixes * **infiniteHits:** fix wrong behavior of showPrevious regarding cachedHits ([#4725](https://github.com/algolia/instantsearch.js/issues/4725)) ([40b27b6](https://github.com/algolia/instantsearch.js/commit/40b27b668ec1dcb8608b299c941e0003b43911d3)) * **ratingMenu:** use url in default template ([#4728](https://github.com/algolia/instantsearch.js/issues/4728)) ([31d9c50](https://github.com/algolia/instantsearch.js/commit/31d9c50344818cd4f4e62993a981ec3616d8b88e)) ### Features * **middleware:** accept partial methods ([#4673](https://github.com/algolia/instantsearch.js/issues/4673)) ([8f2aad2](https://github.com/algolia/instantsearch.js/commit/8f2aad2f0465cc883681143f350a11c24ce694e2)) * **ts:** convert hierarchical-menu to TypeScript ([#4711](https://github.com/algolia/instantsearch.js/issues/4711)) ([870e2f7](https://github.com/algolia/instantsearch.js/commit/870e2f7285d58c48196356cd88fb4aca66feb7aa)) * **ts:** convert RefinementList component to TypeScript ([#4702](https://github.com/algolia/instantsearch.js/issues/4702)) ([fd562de](https://github.com/algolia/instantsearch.js/commit/fd562de5e50e3889abaa9ef8151faa1b5179d7f6)) * **ts:** convert search-box to TypeScript ([#4710](https://github.com/algolia/instantsearch.js/issues/4710)) ([e73257a](https://github.com/algolia/instantsearch.js/commit/e73257a466082207c0289f22bad523334d101aae)) # [4.20.0](https://github.com/algolia/instantsearch.js/compare/v4.19.0...v4.20.0) (2021-04-06) ### Features * **clearRefinements:** implement canRefine ([#4684](https://github.com/algolia/instantsearch.js/issues/4684)) ([a898f09](https://github.com/algolia/instantsearch.js/commit/a898f09bddca5db1f6782104375df3873d49c688)) * **currentRefinements:** implement canRefine ([#4697](https://github.com/algolia/instantsearch.js/issues/4697)) ([4db75ba](https://github.com/algolia/instantsearch.js/commit/4db75baa9ff2e18f871547511d8f1234eea9d41b)) * **hierarchicalMenu:** implement canRefine ([#4685](https://github.com/algolia/instantsearch.js/issues/4685)) ([0d2e450](https://github.com/algolia/instantsearch.js/commit/0d2e450aed2aaac72ae7ff7f1bb322ce6992c8ba)) * **middleware:** add unuse method ([#4708](https://github.com/algolia/instantsearch.js/issues/4708)) ([8e3c406](https://github.com/algolia/instantsearch.js/commit/8e3c406c8f29bcae56d2f82f07cbd087043346fe)) * **pagination:** implement canRefine ([#4683](https://github.com/algolia/instantsearch.js/issues/4683)) ([3ae51e6](https://github.com/algolia/instantsearch.js/commit/3ae51e60543984463a13b25e64aa2f879c91313e)) * **range:** implement canRefine ([#4686](https://github.com/algolia/instantsearch.js/issues/4686)) ([a99ab6f](https://github.com/algolia/instantsearch.js/commit/a99ab6f968b791ffa31cd17dda598c293e73b88e)) * **ratingMenu:** implement canRefine ([#4691](https://github.com/algolia/instantsearch.js/issues/4691)) ([42191a0](https://github.com/algolia/instantsearch.js/commit/42191a097a048a325234dd3f40f7799145628cd6)) * **toggleRefinement:** implement canRefine ([#4689](https://github.com/algolia/instantsearch.js/issues/4689)) ([48dc7f8](https://github.com/algolia/instantsearch.js/commit/48dc7f8423c92b21bcd59856bf2fc685ae4aba69)) * **ts:** convert rating-menu to TypeScript ([#4701](https://github.com/algolia/instantsearch.js/issues/4701)) ([f14ca08](https://github.com/algolia/instantsearch.js/commit/f14ca0891237a7a49b09d881cddedb93efc3a266)) * **ts:** convert Template component to TypeScript ([#4703](https://github.com/algolia/instantsearch.js/issues/4703)) ([0688571](https://github.com/algolia/instantsearch.js/commit/068857137b85d1065bc5997514461d72fe595130)) # [4.19.0](https://github.com/algolia/instantsearch.js/compare/v4.18.0...v4.19.0) (2021-03-30) ### Bug Fixes * **setUiState:** make sure previous ui state is stored ([#4699](https://github.com/algolia/instantsearch.js/issues/4699)) ([0f5d688](https://github.com/algolia/instantsearch.js/commit/0f5d6888c5e77c750d264ed19be3418d920266af)) ### Features * **relevantSort:** implement canRefine ([#4693](https://github.com/algolia/instantsearch.js/issues/4693)) ([24d9ded](https://github.com/algolia/instantsearch.js/commit/24d9ded0c0e3246b91fe16ab1d1d579c17d68731)) * **currentRefinements:** implement canRefine ([#4690](https://github.com/algolia/instantsearch.js/issues/4690)) ([f02416c](https://github.com/algolia/instantsearch.js/commit/f02416cf226ec3f7c2238b3e0902ec6f78381515)) * **ts:** convert sortBy, connectSortBy ([#4700](https://github.com/algolia/instantsearch.js/issues/4700)) ([86de1e0](https://github.com/algolia/instantsearch.js/commit/86de1e0a675c91b75e72463e6b11df62739d69b5)) # [4.18.0](https://github.com/algolia/instantsearch.js/compare/v4.17.0...v4.18.0) (2021-03-24) ### Bug Fixes * **createURL:** correctly remove page in state ([#4679](https://github.com/algolia/instantsearch.js/issues/4679)) ([48c080e](https://github.com/algolia/instantsearch.js/commit/48c080ef85b974e68e1c80ceffea7a0138407a1e)) * **utils:** circular dependency in createSendEventForHits ([#4680](https://github.com/algolia/instantsearch.js/issues/4680)) ([045f33b](https://github.com/algolia/instantsearch.js/commit/045f33bc6184fb04501e39a5a97e1e969095389a)) ### Features * **metadata:** expose client's algolia agent ([#4694](https://github.com/algolia/instantsearch.js/issues/4694)) ([3d0cb5b](https://github.com/algolia/instantsearch.js/commit/3d0cb5b69056674246efb1acf33e143ac7ae4915)) * **ts:** convert connectRefinementList, refinementList ([#4658](https://github.com/algolia/instantsearch.js/issues/4658)) ([794b2d3](https://github.com/algolia/instantsearch.js/commit/794b2d3316ae7ee79cfa0643565b65e5bec5c7c1)) * **ts:** convert stats, connectStats ([#4681](https://github.com/algolia/instantsearch.js/issues/4681)) ([37bbd01](https://github.com/algolia/instantsearch.js/commit/37bbd016a83d5cb66d1f78c0865f7677fa7098fb)) * **ts:** update to typescript 4 ([#4654](https://github.com/algolia/instantsearch.js/issues/4654)) ([638e437](https://github.com/algolia/instantsearch.js/commit/638e437fdd80af0cfd38818f9da37a50f8f4343f)) # [4.17.0](https://github.com/algolia/instantsearch.js/compare/v4.16.1...v4.17.0) (2021-03-09) ### Bug Fixes * **bindEvent:** escape payload correctly ([#4670](https://github.com/algolia/instantsearch.js/issues/4670)) ([c1cbaf4](https://github.com/algolia/instantsearch.js/commit/c1cbaf49f6af9784535df80d024cdad56f3ddb84)) ### Features * **insights:** add hits and attributes to InsightsEvent ([#4667](https://github.com/algolia/instantsearch.js/issues/4667)) ([17ef71c](https://github.com/algolia/instantsearch.js/commit/17ef71c32586d0a93bb3905696b6ff7c7be1f3f9)) ## [4.16.1](https://github.com/algolia/instantsearch.js/compare/v4.16.0...v4.16.1) (2021-03-03) ### Bug Fixes * **relevantSort:** rename smartSort to relevantSort ([#4668](https://github.com/algolia/instantsearch.js/issues/4668)) ([579eee8](https://github.com/algolia/instantsearch.js/commit/579eee8d38effe067407a269e493400c460eb842)) # [4.16.0](https://github.com/algolia/instantsearch.js/compare/v4.15.0...v4.16.0) (2021-03-01) ### Bug Fixes * **relevantSort:** export the widget and the connector ([#4663](https://github.com/algolia/instantsearch.js/issues/4663)) ([e7aaa8c](https://github.com/algolia/instantsearch.js/commit/e7aaa8ceb47b8cafc3a3a323ebe47f45f3841ba4)) ### Features * **answers:** add `EXPERIMENTAL_answers` widget ([#4581](https://github.com/algolia/instantsearch.js/issues/4581)) ([e4c9070](https://github.com/algolia/instantsearch.js/commit/e4c9070250779d7d3afabe7f9a19644717bc12c8)), closes [#4635](https://github.com/algolia/instantsearch.js/issues/4635) # [4.15.0](https://github.com/algolia/instantsearch.js/compare/v4.14.2...v4.15.0) (2021-02-23) ### Features * **relevantSort:** add widget ([#4648](https://github.com/algolia/instantsearch.js/issues/4648)) ([89c6e86](https://github.com/algolia/instantsearch.js/commit/89c6e868f490e9b6e507dd70c215e962f4c69ccb)) * **stats:** apply nbSortedHits ([#4649](https://github.com/algolia/instantsearch.js/issues/4649)) ([34478c1](https://github.com/algolia/instantsearch.js/commit/34478c198dcafbd45fd101db0cd2fbe6328272b8)) * **ts:** convert menu ([#4652](https://github.com/algolia/instantsearch.js/issues/4652)) ([2271b43](https://github.com/algolia/instantsearch.js/commit/2271b4379918e865a1b0cea09c139e517df97bc5)) ## [4.14.2](https://github.com/algolia/instantsearch.js/compare/v4.14.1...v4.14.2) (2021-02-17) ### Bug Fixes * **insights:** don't reset page ([#4655](https://github.com/algolia/instantsearch.js/issues/4655)) ([2b31250](https://github.com/algolia/instantsearch.js/commit/2b312508e8be59284180e7f490ce0aac80f9c2b6)) ## [4.14.1](https://github.com/algolia/instantsearch.js/compare/v4.14.0...v4.14.1) (2021-02-16) ### Bug Fixes * **compat:** remove references to window ([#4651](https://github.com/algolia/instantsearch.js/issues/4651)) ([1ede1ae](https://github.com/algolia/instantsearch.js/commit/1ede1ae392d3a12f5b0fe29075ffeb05e572a874)), closes [#4650](https://github.com/algolia/instantsearch.js/issues/4650) # [4.14.0](https://github.com/algolia/instantsearch.js/compare/v4.13.2...v4.14.0) (2021-02-09) ### Features * **queryRuleContext:** allow to make refinements based on query ([#4638](https://github.com/algolia/instantsearch.js/issues/4638)) ([dd033fc](https://github.com/algolia/instantsearch.js/commit/dd033fc58ff11027e4f4b6157aedf0aea0326af3)) ## [4.13.2](https://github.com/algolia/instantsearch.js/compare/v4.13.1...v4.13.2) (2021-02-03) ### Bug Fixes * **range:** don't go out of bounds with min or max given ([#4627](https://github.com/algolia/instantsearch.js/issues/4627)) ([8327ec0](https://github.com/algolia/instantsearch.js/commit/8327ec01c3940dfc20f5f1c8e3e0fc85f29af690)) ## [4.13.1](https://github.com/algolia/instantsearch.js/compare/v4.13.0...v4.13.1) (2021-01-26) ### Bug Fixes * **index:** only set listeners on init once ([#4634](https://github.com/algolia/instantsearch.js/issues/4634)) ([730b49d](https://github.com/algolia/instantsearch.js/commit/730b49d43782b98c5119a5d3dbfec09073bde1d0)) # [4.13.0](https://github.com/algolia/instantsearch.js/compare/v4.12.0...v4.13.0) (2021-01-26) ### Features * **ratingMenu:** Add support for floats in values ([#4611](https://github.com/algolia/instantsearch.js/issues/4611)) ([3f52784](https://github.com/algolia/instantsearch.js/commit/3f52784862b72ef59acfc0735fe482cbfa6ad1f5)) # [4.12.0](https://github.com/algolia/instantsearch.js/compare/v4.11.0...v4.12.0) (2021-01-20) ### Code Refactoring * rename all references to widgetOptions as widgetParams ([#4612](https://github.com/algolia/instantsearch.js/issues/4612)) ([ff9a18d](https://github.com/algolia/instantsearch.js/commit/ff9a18d31635013ee4bc242291f121c8e5827f38)) ### Features * **core:** expose metadata of widgets ([#4604](https://github.com/algolia/instantsearch.js/issues/4604)) ([1fcf716](https://github.com/algolia/instantsearch.js/commit/1fcf71657b176b14067df36765a38e32d2a6dd9b)) * **widgets:** annotate widget instances with $$widgetType ([#4624](https://github.com/algolia/instantsearch.js/issues/4624)) ([df3f478](https://github.com/algolia/instantsearch.js/commit/df3f47867e65a2e56c6da968d7a154471172adce)) ### BREAKING CHANGES * if you're using experimental-typescript and importing a type of the form `...WidgetOptions`, this now becomes `...WidgetParams` (eg. replace `HitsWidgetOptions` with `HitsWidgetParams`) # [4.11.0](https://github.com/algolia/instantsearch.js/compare/v4.10.0...v4.11.0) (2021-01-14) ### Bug Fixes * **index:** do not warn for nested index widget ([#4620](https://github.com/algolia/instantsearch.js/issues/4620)) ([7502744](https://github.com/algolia/instantsearch.js/commit/7502744cd546181ec4429cd6b8144200ba2a8f82)) * **insights:** don't quote values ([#4619](https://github.com/algolia/instantsearch.js/issues/4619)) ([ac2444c](https://github.com/algolia/instantsearch.js/commit/ac2444c36c6f41e35ed6d1a6d045479b35416576)) ### Features * **insights:** accept initParams for insightsClient ([#4608](https://github.com/algolia/instantsearch.js/issues/4608)) ([0a0ae2b](https://github.com/algolia/instantsearch.js/commit/0a0ae2bf10a4e210373b8fde635949a56c86e52e)) # [4.10.0](https://github.com/algolia/instantsearch.js/compare/v4.9.2...v4.10.0) (2021-01-05) ### Features * **index:** expose createURL ([#4603](https://github.com/algolia/instantsearch.js/issues/4603)) ([f57e9c5](https://github.com/algolia/instantsearch.js/commit/f57e9c5a46e927b8dd38f167ee5c467151334a08)) * **index:** expose scoped results getter ([#4609](https://github.com/algolia/instantsearch.js/issues/4609)) ([a41b1e4](https://github.com/algolia/instantsearch.js/commit/a41b1e46bb195e6ef1f9bdbdde64d9300246c22f)) * **reverseHighlight/reverseSnippet:** Implements reverseHighlight and reverseSnippet ([#4592](https://github.com/algolia/instantsearch.js/issues/4592)) ([718bf45](https://github.com/algolia/instantsearch.js/commit/718bf458152bb55bab1efb542adb8e31298c0c3c)) ## [4.9.2](https://github.com/algolia/instantsearch.js/compare/v4.9.1...v4.9.2) (2020-12-15) ### Bug Fixes * warn about invalid userToken ([#4605](https://github.com/algolia/instantsearch.js/issues/4605)) ([5fce769](https://github.com/algolia/instantsearch.js/commit/5fce769f42fe5b44f73eb68f3858a6ea1ec2d854)) * **types:** correct type for queryHook return ([#4602](https://github.com/algolia/instantsearch.js/issues/4602)) ([acff8db](https://github.com/algolia/instantsearch.js/commit/acff8db3a2238edf40da1ee6b44e93a94e090698)) ## [4.9.1](https://github.com/algolia/instantsearch.js/compare/v4.9.0...v4.9.1) (2020-12-08) ### Bug Fixes * **range:** consistently convert min & max to numbers ([#4587](https://github.com/algolia/instantsearch.js/issues/4587)) ([ccf159e](https://github.com/algolia/instantsearch.js/commit/ccf159efcb94e9c8c04c558fcb69e2e3d8d79729)) # [4.9.0](https://github.com/algolia/instantsearch.js/compare/v4.8.7...v4.9.0) (2020-12-01) ### Bug Fixes * remove a warning about insights that is not relevant anymore ([#4593](https://github.com/algolia/instantsearch.js/issues/4593)) ([b5f6a47](https://github.com/algolia/instantsearch.js/commit/b5f6a479ff1b9b692c733f51e39eade724ff3413)) ### Features * **autocomplete:** implement `getWidgetRenderState` ([#4466](https://github.com/algolia/instantsearch.js/issues/4466)) ([c215836](https://github.com/algolia/instantsearch.js/commit/c2158364a63d0f05bb820f802871a2f093e041ec)) * **breadcrumb:** implement `getWidgetRenderState` ([#4467](https://github.com/algolia/instantsearch.js/issues/4467)) ([80b348e](https://github.com/algolia/instantsearch.js/commit/80b348ef1a6a29b1897f5ee1d680dcbaba5fa4fe)) * **clearRefinements:** implement `getWidgetRenderState` ([#4468](https://github.com/algolia/instantsearch.js/issues/4468)) ([2b3117c](https://github.com/algolia/instantsearch.js/commit/2b3117c34207514967ff453b6f5d8275a6b0b0ec)) * **configure:** getRenderState for multiple configure widgets ([#4582](https://github.com/algolia/instantsearch.js/issues/4582)) ([5432af1](https://github.com/algolia/instantsearch.js/commit/5432af1df3c1ee4e62b87ede76acda7b749f38dd)) * **configure:** implement `getWidgetRenderState` ([#4469](https://github.com/algolia/instantsearch.js/issues/4469)) ([3a1b325](https://github.com/algolia/instantsearch.js/commit/3a1b32556f3d5a6a3330b404688e06d5815a2390)) * **connectPagination:** add getWidgetRenderState & refactor to TS ([#4574](https://github.com/algolia/instantsearch.js/issues/4574)) ([1553aa3](https://github.com/algolia/instantsearch.js/commit/1553aa36c8bb8664b5e74fd2378ea2ef45a52acf)) * **core:** introduce `getWidgetRenderState` (2/n) ([#4457](https://github.com/algolia/instantsearch.js/issues/4457)) ([4839bb6](https://github.com/algolia/instantsearch.js/commit/4839bb61e4c8ee6083710195d5db5684c7b0889f)) * **core:** introduce `getWidgetUiState` lifecycle hook (1/n) ([#4454](https://github.com/algolia/instantsearch.js/issues/4454)) ([cf21ea4](https://github.com/algolia/instantsearch.js/commit/cf21ea4cb580ed523828c926b7ba724c46eed8a4)) * **currentRefinements:** implement `getWidgetRenderState` ([#4470](https://github.com/algolia/instantsearch.js/issues/4470)) ([b8df824](https://github.com/algolia/instantsearch.js/commit/b8df824e26a164280d9da9b3c3ce41ad56962439)) * **connectQueryRules:** getWidgetRenderState ([#4572](https://github.com/algolia/instantsearch.js/issues/4572)) ([edcc4a4](https://github.com/algolia/instantsearch.js/commit/edcc4a463d32af21bb73acbca879d4982ae9006f)) * **connectGeoSearch:** support getWidgetRenderState ([#4564](https://github.com/algolia/instantsearch.js/issues/4564)) ([8d06fba](https://github.com/algolia/instantsearch.js/commit/8d06fba40be0392daa1b48f235d93d92bb6b5e93)) * **hierarchicalMenu:** implement `getWidgetRenderState` ([#4471](https://github.com/algolia/instantsearch.js/issues/4471)) ([9fd3cd0](https://github.com/algolia/instantsearch.js/commit/9fd3cd06dfc3b5302c00ee1820ff58be2a37c3b7)) * **highlight:** accept array for attribute ([#4588](https://github.com/algolia/instantsearch.js/issues/4588)) ([b0c3a3a](https://github.com/algolia/instantsearch.js/commit/b0c3a3a960646bff22b2d28e21aa2675484a354b)) * **hits:** implement `getWidgetRenderState` ([#4525](https://github.com/algolia/instantsearch.js/issues/4525)) ([3391ff7](https://github.com/algolia/instantsearch.js/commit/3391ff7bac8b406ab474e712408bda2be69934c9)) * **hitsPerPage:** implement `getRenderState` and `getWidgetRenderState` ([#4532](https://github.com/algolia/instantsearch.js/issues/4532)) ([7ad10ea](https://github.com/algolia/instantsearch.js/commit/7ad10ea648f48766061153994da90920a5194103)) * **infinite-hits:** implement `getRenderState` and `getWidgetRenderState` ([#4535](https://github.com/algolia/instantsearch.js/issues/4535)) ([98c70d9](https://github.com/algolia/instantsearch.js/commit/98c70d980bc1036057a2dd99dc6aeee8343e4472)) * **menu:** implement `getRenderState` and `getWidgetRenderState` ([#4540](https://github.com/algolia/instantsearch.js/issues/4540)) ([239906c](https://github.com/algolia/instantsearch.js/commit/239906c7fdb36c691b9a9aca343802a8ccc616c8)) * **panel:** spread widgetRenderState in the options in panel ([#4527](https://github.com/algolia/instantsearch.js/issues/4527)) ([8f82eaa](https://github.com/algolia/instantsearch.js/commit/8f82eaa34e7abe9070e404a5a45d352af61d940a)), closes [#4558](https://github.com/algolia/instantsearch.js/issues/4558) * **poweredBy:** getWidgetRenderState ([#4551](https://github.com/algolia/instantsearch.js/issues/4551)) ([cd816a4](https://github.com/algolia/instantsearch.js/commit/cd816a41afe0704eab3cbd1f019fc660ca5d255e)) * **range:** implement `getRenderState` and `getWidgetRenderState` ([#4536](https://github.com/algolia/instantsearch.js/issues/4536)) ([d67bfcd](https://github.com/algolia/instantsearch.js/commit/d67bfcdb828cc8b35a5c959e54823b6d3c37b087)) * **rating-menu:** implement `getRenderState` and `getWidgetRenderState` ([#4548](https://github.com/algolia/instantsearch.js/issues/4548)) ([166a96c](https://github.com/algolia/instantsearch.js/commit/166a96c170c137e78b3fe3b9f69f73744f4fcb8b)) * **refinement-list:** implement `getRenderState` and `getWidgetRenderState` ([#4549](https://github.com/algolia/instantsearch.js/issues/4549)) ([c824bd0](https://github.com/algolia/instantsearch.js/commit/c824bd074d388e44e99b53592167cffcacae3377)) * **numeric-menu:** add `getRenderState` ([#4550](https://github.com/algolia/instantsearch.js/issues/4550)) ([5385edf](https://github.com/algolia/instantsearch.js/commit/5385edf39d3ac1515845b5e20ce179a2869ab86d)) * **sortBy:** implement `getRenderState` and `getWidgetRenderState` ([#4568](https://github.com/algolia/instantsearch.js/issues/4568)) ([fd249f7](https://github.com/algolia/instantsearch.js/commit/fd249f700854d1f11e97cb5dac2c1b3964c59e29)) * **stats:** implement `getRenderState` and `getWidgetRenderState` ([#4565](https://github.com/algolia/instantsearch.js/issues/4565)) ([b8dfd6d](https://github.com/algolia/instantsearch.js/commit/b8dfd6dbb8c462b0d0571e9f0499df6e4dda7745)) * **toggleRefinement:** implement `getRenderState` and `getWidgetRenderState` ([#4569](https://github.com/algolia/instantsearch.js/issues/4569)) ([f2c9a10](https://github.com/algolia/instantsearch.js/commit/f2c9a102cba9abe21ed08b18e979713156e10901)) * **voice-search:** implement `getRenderState` and `getWidgetRenderState` ([#4557](https://github.com/algolia/instantsearch.js/issues/4557)) ([d308da1](https://github.com/algolia/instantsearch.js/commit/d308da1ab892cc5185616cd5b8a4a3f488e708c4)) ## [4.8.7](https://github.com/algolia/instantsearch.js/compare/v4.8.6...v4.8.7) (2020-11-19) ### Bug Fixes * **insights:** use internal `find` util method ([#4580](https://github.com/algolia/instantsearch.js/issues/4580)) ([61b855b](https://github.com/algolia/instantsearch.js/commit/61b855b28282992a55795db88f8bfef2e5825cb3)) ## [4.8.6](https://github.com/algolia/instantsearch.js/compare/v4.8.5...v4.8.6) (2020-11-17) ### Bug Fixes * **insights:** do not throw when sending event right after creating insights middleware ([#4575](https://github.com/algolia/instantsearch.js/issues/4575)) ([d963f8d](https://github.com/algolia/instantsearch.js/commit/d963f8d6155e6bb56f852e00528ed10dc9bcc461)) ## [4.8.5](https://github.com/algolia/instantsearch.js/compare/v4.8.4...v4.8.5) (2020-11-10) ### Bug Fixes * **configure:** pass the latest state to onStateChange ([#4555](https://github.com/algolia/instantsearch.js/issues/4555)) ([6ab76e8](https://github.com/algolia/instantsearch.js/commit/6ab76e82f93e8c7bb2bfdde267b6d7f4f9b333ff)) ## [4.8.4](https://github.com/algolia/instantsearch.js/compare/v4.8.3...v4.8.4) (2020-10-27) ### Bug Fixes * **infiniteHits:** do not cache the cached hits inside the connector ([#4534](https://github.com/algolia/instantsearch.js/issues/4534)) ([c97395e](https://github.com/algolia/instantsearch.js/commit/c97395e2d3443651e628617f0974703a100a988e)) * **insights:** show deprecation warnings for old insights related properties and functions ([#4524](https://github.com/algolia/instantsearch.js/issues/4524)) ([c93e1cf](https://github.com/algolia/instantsearch.js/commit/c93e1cfcad06b327066078088410eb7d51972790)) ## [4.8.3](https://github.com/algolia/instantsearch.js/compare/v4.8.2...v4.8.3) (2020-09-29) ### Bug Fixes * **middleware:** rename EXPERIMENTAL_use to use ([#4450](https://github.com/algolia/instantsearch.js/issues/4450)) ([87ecb99](https://github.com/algolia/instantsearch.js/commit/87ecb99f33ab4930d8ec1996ddba9db0a9d07da4)) * **refinementList:** cap `maxFacetHits` to 100 for SFFV ([#4523](https://github.com/algolia/instantsearch.js/issues/4523)) ([baf1f02](https://github.com/algolia/instantsearch.js/commit/baf1f027fc2436e86536fffbee11a595cfd7dac0)) ## [4.8.2](https://github.com/algolia/instantsearch.js/compare/v4.8.1...v4.8.2) (2020-09-22) ### Bug Fixes * **insights:** fix the regression that it didn't send events with instantsearch.insights() ([#4519](https://github.com/algolia/instantsearch.js/issues/4519)) ([10e38df](https://github.com/algolia/instantsearch.js/commit/10e38df02608071cd7272e829b6748be41b9c2c0)) ## [4.8.1](https://github.com/algolia/instantsearch.js/compare/v4.8.0...v4.8.1) (2020-09-15) ### Bug Fixes * **hitsPerPage:** update link to hitsPerPage widget ([#4513](https://github.com/algolia/instantsearch.js/issues/4513)) ([daa4bb9](https://github.com/algolia/instantsearch.js/commit/daa4bb944065dede46d716308325039c3602d9dc)) * **infiniteHits:** compute `isLastPage` based on cached pages ([#4509](https://github.com/algolia/instantsearch.js/issues/4509)) ([b6fb1ab](https://github.com/algolia/instantsearch.js/commit/b6fb1abcf5ac456dc39adaeb97945665cad8fa11)) # [4.8.0](https://github.com/algolia/instantsearch.js/compare/v4.7.2...v4.8.0) (2020-09-08) ### Features * **insights:** introduce `insights` middleware ([#4446](https://github.com/algolia/instantsearch.js/issues/4446)) ([9bc6359](https://github.com/algolia/instantsearch.js/commit/9bc635986097736272aac8c5d3380a255488fdb7)) ## [4.7.2](https://github.com/algolia/instantsearch.js/compare/v4.7.1...v4.7.2) (2020-08-31) ### Bug Fixes * **bundlesize:** remove prop-type imports ([#4491](https://github.com/algolia/instantsearch.js/issues/4491)) ([8361cd6](https://github.com/algolia/instantsearch.js/commit/8361cd63b3bac15eb6250e9f509fb15c1fc57f48)) * **router:** skip router write on duplicate entries ([#4487](https://github.com/algolia/instantsearch.js/issues/4487)) ([9296022](https://github.com/algolia/instantsearch.js/commit/9296022fecadfbf82f15e837c215a1356eac4bc5)) * **searchBox:** pass "spellcheck" property correctly to input ([#4483](https://github.com/algolia/instantsearch.js/issues/4483)) ([3cf43c7](https://github.com/algolia/instantsearch.js/commit/3cf43c7187841cf961a0280307af1a5f7a4e8da7)) # [4.7.1](https://github.com/algolia/instantsearch.js/compare/v4.7.0...v4.7.1) (2020-08-19) ### Bug Fixes * **configureRelatedItems:** support nested attributes ([#4480](https://github.com/algolia/instantsearch.js/issues/4480)) ([2266004](https://github.com/algolia/instantsearch.js/commit/2266004f274138b45640f000a5da8aa14e419e6c)) * **connectToggleRefinement:** fix onFacetValue/offFacetValue on render when using arrays for on/off ([#4449](https://github.com/algolia/instantsearch.js/issues/4449)) ([fd3e83f](https://github.com/algolia/instantsearch.js/commit/fd3e83f2cf2e5b44b7d29eb4c67526e55c18d708)) * **index:** don't show a development warning for inconsistent UI state in `connectRange` ([#4440](https://github.com/algolia/instantsearch.js/issues/4440)) ([eb8c8b3](https://github.com/algolia/instantsearch.js/commit/eb8c8b3494cb66dbef1d03e7d74374dc49059345)), closes [#4437](https://github.com/algolia/instantsearch.js/issues/4437) * **infiniteHits:** work with controlled mode ([#4435](https://github.com/algolia/instantsearch.js/issues/4435)) ([68b20f4](https://github.com/algolia/instantsearch.js/commit/68b20f487fcd54fd7dec11b4c494b6aa94a18516)) * **typescript:** correct dummy v4 client ([#4459](https://github.com/algolia/instantsearch.js/issues/4459)) ([ca0c394](https://github.com/algolia/instantsearch.js/commit/ca0c3946608bb8ec5dcf5378d8d382d809a4d86f)) * **typescript:** jsDoc comments which conform to Connector definition ([#4458](https://github.com/algolia/instantsearch.js/issues/4458)) ([5209bdb](https://github.com/algolia/instantsearch.js/commit/5209bdb9189e7cbbf9514b62fde55f923b2b3273)) * **typescript:** export correct types ([#4476](https://github.com/algolia/instantsearch.js/issues/4476)) ([5fb4c5b](https://github.com/algolia/instantsearch.js/commit/5fb4c5b9d6ac75636e94514598ef5d5a86affafd)) # [4.7.0](https://github.com/algolia/instantsearch.js/compare/v4.6.0...v4.7.0) (2020-06-15) ### Bug Fixes * **rangeInput:** clear input when refinement is cleared ([#4429](https://github.com/algolia/instantsearch.js/issues/4429)) ([a2c7663](https://github.com/algolia/instantsearch.js/commit/a2c7663424c5cd59e17ed841e12abaa19e524b14)) ### Features * **infiniteHits:** support cache ([#4431](https://github.com/algolia/instantsearch.js/issues/4431)) ([008c01c](https://github.com/algolia/instantsearch.js/commit/008c01c7cd09e4fcecdf53a4b299960de2b7a026)) # [4.6.0](https://github.com/algolia/instantsearch.js/compare/v4.5.0...v4.6.0) (2020-06-08) ### Bug Fixes * **connectPagination:** set `isLastPage` to `true` when no results ([#4422](https://github.com/algolia/instantsearch.js/issues/4422)) ([92bcc02](https://github.com/algolia/instantsearch.js/commit/92bcc0271927f0239083366fff920530977e32cd)) * **rangeInput:** support typing float numbers ([#4418](https://github.com/algolia/instantsearch.js/issues/4418)) ([61b19b8](https://github.com/algolia/instantsearch.js/commit/61b19b87ae3afdabde8ef355e3b727059ae59911)) ### Features * **connectToggleRefinement:** add support for array values ([#4420](https://github.com/algolia/instantsearch.js/issues/4420)) ([fe1fbee](https://github.com/algolia/instantsearch.js/commit/fe1fbee4ad59c5f24831ed38a419906bbd7d2c15)) # [4.5.0](https://github.com/algolia/instantsearch.js/compare/v4.4.1...v4.5.0) (2020-05-13) ### Bug Fixes * **middleware:** subscribe middleware after `init` ([#4322](https://github.com/algolia/instantsearch.js/issues/4322)) ([f61fc4d](https://github.com/algolia/instantsearch.js/commit/f61fc4d133c118cfe8f2a2ba2e02d037a21cf8e0)) ### Features * **index:** support adding index widget with initial UI state ([#4359](https://github.com/algolia/instantsearch.js/issues/4359)) ([5ff4c83](https://github.com/algolia/instantsearch.js/commit/5ff4c8307c2be7bde7fb53aa9935a243e6532fe2)) * **voice:** allow custom voice helper ([#4363](https://github.com/algolia/instantsearch.js/issues/4363)) ([4a00fa6](https://github.com/algolia/instantsearch.js/commit/4a00fa607354aefaae468735b590e237a2d46f9b)) ## [4.4.1](https://github.com/algolia/instantsearch.js/compare/v4.4.0...v4.4.1) (2020-04-29) ### Bug Fixes * **range:** fix range calculation when step is set ([#4398](https://github.com/algolia/instantsearch.js/issues/4398)) ([a36b4e0](https://github.com/algolia/instantsearch.js/commit/a36b4e0a64afaa9dfa3048c802d010d569c821a9)) * **router:** don't write an existing URL ([#4392](https://github.com/algolia/instantsearch.js/issues/4392)) ([ee6a9c6](https://github.com/algolia/instantsearch.js/commit/ee6a9c657c97adebba9fb9404eae454c3996b86d)) # [4.4.0](https://github.com/algolia/instantsearch.js/compare/v4.3.1...v4.4.0) (2020-04-08) ### Features * introduce controlled mode APIs with `onStateChange` and `setUiState` ([#4362](https://github.com/algolia/instantsearch.js/issues/4362)) ([4953324](https://github.com/algolia/instantsearch.js/commit/4953324ac8a3af4c6a8be411ca9e7cc673ee6561)) ## [4.3.1](https://github.com/algolia/instantsearch.js/compare/v4.3.0...v4.3.1) (2020-03-06) This versions fixes a [Cross-Site Scripting](https://en.wikipedia.org/wiki/Cross-site_scripting) (XSS) vulnerability ([#4344](https://github.com/algolia/instantsearch.js/issues/4344)) when using the [`refinementList`](https://www.algolia.com/doc/api-reference/widgets/refinement-list/js/) widget when relying on its default [`item`](https://www.algolia.com/doc/api-reference/widgets/refinement-list/js/#widget-param-item) template and [routing](https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/#widget-param-routing). **We recommend all users to upgrade to this version**. We now escape the `refinementList` `item` template by default, which avoids HTML to be injected. If ever you were relying on this behavior, **which we do not recommend**, you can copy the [previous `item` template](https://github.com/algolia/instantsearch.js/blob/933d9ffb3c0a396a047eeb4b44733b17aa31d081/src/widgets/refinement-list/defaultTemplates.js#L2-L9) into your widget. You were not vulnerable to this XSS if: - You didn't use [routing](https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/#widget-param-routing) - You didn't use use the [`refinementList`](https://www.algolia.com/doc/api-reference/widgets/refinement-list/js/) widget ([`connectRefinementList`](https://www.algolia.com/doc/api-reference/widgets/refinement-list/js/#connector) is not subject to this issue) - You used a custom `item` template for your [`refinementList`](https://www.algolia.com/doc/api-reference/widgets/refinement-list/js/) widget that does not rely on the triple-brace Hogan.js syntax (e.g., `{{{highlighted}}}`) ### Bug Fixes * **refinementList:** prevent XSS via routing ([#4344](https://github.com/algolia/instantsearch.js/issues/4344)) ([8552221](https://github.com/algolia/instantsearch.js/commit/8552221eff17a4ae5ba9c454054b0eb6e002934d)) # [4.3.0](https://github.com/algolia/instantsearch.js/compare/v4.2.0...v4.3.0) (2020-02-25) ### Bug Fixes * **deps:** update dependency algoliasearch-helper to v3.1.1 that fixes a case where refinements for a facet with a name that matches a substring of another facet could be cleared by mistake ([algolia/algoliasearch-helper-js/pull/760](https://github.com/algolia/algoliasearch-helper-js/pull/760)) ([#4335](https://github.com/algolia/instantsearch.js/issues/4335)) ([9bc66cf](https://github.com/algolia/instantsearch.js/commit/381cda05c9c51dc9d3245a6d926e3c919245b723)) ### Features * **highlight:** add cssClasses to snippet & highlight helper ([#4306](https://github.com/algolia/instantsearch.js/issues/4306)) ([ece0aa6](https://github.com/algolia/instantsearch.js/commit/ece0aa60f05c2c687a23f9219d62ace0d5b866f9)) # [4.2.0](https://github.com/algolia/instantsearch.js/compare/v4.1.1...v4.2.0) (2020-01-23) ### Features * **algoliasearch:** add support for algoliasearch v4 ([#4294](https://github.com/algolia/instantsearch.js/issues/4294)) ([73f1584](https://github.com/algolia/instantsearch.js/commit/73f158428c7d4de1e3d1bc40bf4342362f275829)) * **insights:** add getInsightsAnonymousUserToken helper ([#4279](https://github.com/algolia/instantsearch.js/issues/4279)) ([4653f95](https://github.com/algolia/instantsearch.js/commit/4653f95b436a0715ce1489e0b83c00a87e4a02f0)) ## [4.1.1](https://github.com/algolia/instantsearch.js/compare/v4.1.0...v4.1.1) (2019-12-20) ### Bug Fixes * **configureRelatedItems:** use `facetFilters` to exclude `obje… ([#4264](https://github.com/algolia/instantsearch.js/issues/4264)) ([9bc66cf](https://github.com/algolia/instantsearch.js/commit/9bc66cfb8b13a44840c687a1631696c85e45845f)) * **index:** fix warning for widgets sharing connectors ([#4260](https://github.com/algolia/instantsearch.js/issues/4260)) ([ec97b4a](https://github.com/algolia/instantsearch.js/commit/ec97b4a07e5d1f9a967f5ee5925ebd3b447e1b02)) * **insights:** export Insights helper in the ESM build ([#4261](https://github.com/algolia/instantsearch.js/issues/4261)) ([20649af](https://github.com/algolia/instantsearch.js/commit/20649aff54a3150050866038cd3718d6010c353b)) * **insights:** move 'insightsClient not provided error' to wrapper level ([#4254](https://github.com/algolia/instantsearch.js/issues/4254)) ([15d38dd](https://github.com/algolia/instantsearch.js/commit/15d38ddb87fbd6323f350d42f791c4d7a1505eeb)) ### Features * **insights:** add hogan helper ([#4253](https://github.com/algolia/instantsearch.js/issues/4253)) ([85739d7](https://github.com/algolia/instantsearch.js/commit/85739d782ae1fad3b87612e4a410eada0ca4fe54)) # [4.1.0](https://github.com/algolia/instantsearch.js/compare/v4.0.1...v4.1.0) (2019-12-10) The [4.0.1](#4.0.1) release contained experimental TypeScript definitions in the ESM build by accident. We rolled this back in 4.1.0 because types will first be released on an experimental tag: `experimental-typescript`. ### Bug Fixes * **core:** display correct object types in messages ([#4249](https://github.com/algolia/instantsearch.js/issues/4249)) ([fb2c3c9](https://github.com/algolia/instantsearch.js/commit/fb2c3c9c37fd8d28cd4712486c5c637e237fe83b)) * **insights:** detect clicks on children of `[data-insights]` HTML elements ([#4197](https://github.com/algolia/instantsearch.js/issues/4197)) ([9cac5a3](https://github.com/algolia/instantsearch.js/commit/9cac5a3aa4af616ec7913c17ed7388134c5e7f0a)) * **insights:** display docs URL when missing ([#4231](https://github.com/algolia/instantsearch.js/issues/4231)) ([9df1e7f](https://github.com/algolia/instantsearch.js/commit/9df1e7f762333bd31b5840b35378d56605fe4844)) * **widgets:** override connectors' `$$type` ([#4227](https://github.com/algolia/instantsearch.js/issues/4227)) ([50f4af3](https://github.com/algolia/instantsearch.js/commit/50f4af3006a44cd08dd99b3a72bd410340c2e48a)) ### Features * **middleware:** introduce `EXPERIMENTAL_use` to plug middleware into InstantSearch ([#4224](https://github.com/algolia/instantsearch.js/issues/4224)) ([9d1f7be](https://github.com/algolia/instantsearch.js/commit/9d1f7be9df304a4bc2d07dbd253a73580a0593c3)) * **router:** plug router as a middleware ([#4224](https://github.com/algolia/instantsearch.js/issues/4224)) ([9d1f7be](https://github.com/algolia/instantsearch.js/commit/9d1f7be9df304a4bc2d07dbd253a73580a0593c3)) * **insights:** detect window.aa when available on global scope and a function ([#4191](https://github.com/algolia/instantsearch.js/issues/4191)) ([d6df5af](https://github.com/algolia/instantsearch.js/commit/d6df5affc4111aaf2c82f847ffe877793faac86c)) * **typescript:** add declaration files (experimental) ([#4220](https://github.com/algolia/instantsearch.js/issues/4220)) ([ebacfe5](https://github.com/algolia/instantsearch.js/commit/ebacfe55bc0fddf9ca217eca8c8a207b220ab93d)) * **widgets:** introduce Related Items widgets as experimental (`EXPERIMENTAL_configureRelatedItems` and `EXPERIMENTAL_connectConfigureRelatedItems`) ([#4233](https://github.com/algolia/instantsearch.js/issues/4233)) ([f811f4e](https://github.com/algolia/instantsearch.js/commit/f811f4efa3e58a2b868d11ec338248715a7596c9)) ## [4.0.1](https://github.com/algolia/instantsearch.js/compare/v4.0.0...v4.0.1) (2019-11-28) ### Bug Fixes * widget name in documentation link for index ([#4172](https://github.com/algolia/instantsearch.js/issues/4172)) ([fe7e588](https://github.com/algolia/instantsearch.js/commit/fe7e588d252ad6bd7de2f49d52ca022099f3e959)) * **helper:** rely on stable version of algoliasearch-helper ([#4200](https://github.com/algolia/instantsearch.js/issues/4200)) ([ff11731](https://github.com/algolia/instantsearch.js/commit/ff117314d786c4509edabcb1ddbac73f55930511)) * **infiniteHits:** correct widget options types ([#4222](https://github.com/algolia/instantsearch.js/issues/4222)) ([bb1b327](https://github.com/algolia/instantsearch.js/commit/bb1b327e26b5faad3358a00d174dc48fd4b73356)) * **queryHook:** restore behaviour of queryHook ([#4202](https://github.com/algolia/instantsearch.js/issues/4202)) ([7bf96cb](https://github.com/algolia/instantsearch.js/commit/7bf96cb6eafd5349cdf2f32114d5e6ef5dde1328)), closes [/github.com/algolia/instantsearch.js/commit/c073a9acb51fff3c15278fcd563e47fec55c8365#diff-530222e0c4597f2110dc6ba173a306b0L98](https://github.com//github.com/algolia/instantsearch.js/commit/c073a9acb51fff3c15278fcd563e47fec55c8365/issues/diff-530222e0c4597f2110dc6ba173a306b0L98) ### Features * **transformers:** add tests ([#4153](https://github.com/algolia/instantsearch.js/issues/4153)) ([5a28415](https://github.com/algolia/instantsearch.js/commit/5a28415c39bf5a3a65c61d8f0d444ea6f4e0e17a)) # [4.0.0](https://github.com/algolia/instantsearch.js/compare/v3.7.0...v4.0.0) (2019-10-23) This release is focused on two main features: Federated search, and bundle size reduction. Federated search, is the feature where you search through multiple types of content with the same experience, but with separate result lists. In the past we have also called this feature "multi-index search". This feature helps you make more efficient UIs with multiple result lists, autocomplete, nested interfaces and query suggestions. You can read more about the new index widget [in the documentation](https://www.algolia.com/doc/api-reference/widgets/index-widget/js/). The second main feature is bundle size reduction. This is a bottom-up process where we started by removing Lodash from our bundle. While the library has many useful features, it was a major part of our compiled code. We have also updated to Preact X, the latest version of Preact internally. This allows us to use more modern (p)react features in the future, which have a more efficient bundling pattern. You can read more details on our choices by following [the original posts](https://discourse.algolia.com/t/instantsearch-js-v4-beta-0-is-released/8461) about the beta releases. Even though all this internally were major refactors, this should not have a big impact on how you are using InstantSearch. For the few things which did change, a migration guide can be found in [the documentation](https://www.algolia.com/doc/guides/building-search-ui/upgrade-guides/js/#upgrade-from-v3-to-v4). Don't hesitate to reach out if anything is unclear from that guide, so we can fix it for everyone. Note, if you are using the [places.js](https://github.com/algolia/places) InstantSearch widget, it is not compatible with InstantSearch v4. However, we took this opportunity to make it a real part of InstantSearch.js, and is now accessible as a widget of InstantSearch. You can use it with a `placesReference`. ### Bug Fixes * **configure:** merge with the previous parameters ([#4085](https://github.com/algolia/instantsearch.js/issues/4085)) ([a215d0c](https://github.com/algolia/instantsearch.js/commit/a215d0c)) * **configure:** update lifecycle state ([#3994](https://github.com/algolia/instantsearch.js/issues/3994)) ([3d8d967](https://github.com/algolia/instantsearch.js/commit/3d8d967)) * **connectInfiniteHits:** fix page state when adding or removing widgets ([#4104](https://github.com/algolia/instantsearch.js/issues/4104)) ([1077340](https://github.com/algolia/instantsearch.js/commit/1077340)) * **connectInfiniteHits:** fix state when navigating or adding/removing widgets ([#4123](https://github.com/algolia/instantsearch.js/issues/4123)) ([9cbd24a](https://github.com/algolia/instantsearch.js/commit/9cbd24a)) * **createURL:** support multi-index ([#4082](https://github.com/algolia/instantsearch.js/issues/4082)) ([179a6e5](https://github.com/algolia/instantsearch.js/commit/179a6e5)) * **defer:** recover from error ([#3933](https://github.com/algolia/instantsearch.js/issues/3933)) ([f22b9e2](https://github.com/algolia/instantsearch.js/commit/f22b9e2)) * **helper:** expose .lastResults to .helper ([#4170](https://github.com/algolia/instantsearch.js/issues/4170)) ([236eb7b](https://github.com/algolia/instantsearch.js/commit/236eb7b)) * **history:** avoid empty query string ([#4130](https://github.com/algolia/instantsearch.js/issues/4130)) ([18fee7c](https://github.com/algolia/instantsearch.js/commit/18fee7c)) * **hits:** update lifecycle state ([#3977](https://github.com/algolia/instantsearch.js/issues/3977)) ([6e55ba6](https://github.com/algolia/instantsearch.js/commit/6e55ba6)) * **hitsPerPage:** avoid sync default value ([#4086](https://github.com/algolia/instantsearch.js/issues/4086)) ([3f8b958](https://github.com/algolia/instantsearch.js/commit/3f8b958)) * **hitsPerPage:** update lifecycle state ([#3978](https://github.com/algolia/instantsearch.js/issues/3978)) ([d21d620](https://github.com/algolia/instantsearch.js/commit/d21d620)) * **index:** ensure that we always use the index set by widgets ([#4125](https://github.com/algolia/instantsearch.js/issues/4125)) ([952dc70](https://github.com/algolia/instantsearch.js/commit/952dc70)), closes [/github.com/algolia/algoliasearch-helper-js/blob/5a0352aa233c5ea932df6b054a16989c8d302404/src/algoliasearch.helper.js#L124](https://github.com//github.com/algolia/algoliasearch-helper-js/blob/5a0352aa233c5ea932df6b054a16989c8d302404/src/algoliasearch.helper.js/issues/L124) * **index:** prevent render without results ([#3932](https://github.com/algolia/instantsearch.js/issues/3932)) ([1b9b5f4](https://github.com/algolia/instantsearch.js/commit/1b9b5f4)) * **index:** subscribe to state change only after init for uiState ([#4003](https://github.com/algolia/instantsearch.js/issues/4003)) ([9490ca9](https://github.com/algolia/instantsearch.js/commit/9490ca9)) * **index:** support custom UI params in UI state warning ([#4165](https://github.com/algolia/instantsearch.js/issues/4165)) ([80d32fc](https://github.com/algolia/instantsearch.js/commit/80d32fc)) * **index:** warn for inconsistent UI state in development mode ([#4140](https://github.com/algolia/instantsearch.js/issues/4140)) ([7e277dc](https://github.com/algolia/instantsearch.js/commit/7e277dc)) * **infiniteHits:** update lifecycle state ([#3983](https://github.com/algolia/instantsearch.js/issues/3983)) ([4b8bee5](https://github.com/algolia/instantsearch.js/commit/4b8bee5)) * **instantsearch:** return instance in widgets methods ([#4143](https://github.com/algolia/instantsearch.js/issues/4143)) ([77ffb93](https://github.com/algolia/instantsearch.js/commit/77ffb93)) * **InstantSearch:** cancel scheduled operations ([#3930](https://github.com/algolia/instantsearch.js/issues/3930)) ([3aafbad](https://github.com/algolia/instantsearch.js/commit/3aafbad)) * **InstantSearch:** fix initialUIState when refinements are already present in the route ([#4103](https://github.com/algolia/instantsearch.js/issues/4103)) ([079db57](https://github.com/algolia/instantsearch.js/commit/079db57)) * **InstantSearch:** remove useless walk/duplicate request ([#4127](https://github.com/algolia/instantsearch.js/issues/4127)) ([70163a8](https://github.com/algolia/instantsearch.js/commit/70163a8)) * **menu:** apply & remove refinement ([#4027](https://github.com/algolia/instantsearch.js/issues/4027)) ([85de2cf](https://github.com/algolia/instantsearch.js/commit/85de2cf)) * **menu:** prevent error on stale search ([#3934](https://github.com/algolia/instantsearch.js/issues/3934)) ([5f9e138](https://github.com/algolia/instantsearch.js/commit/5f9e138)) * **numericMenu:** take array into account for empty state ([#4084](https://github.com/algolia/instantsearch.js/issues/4084)) ([2c05a01](https://github.com/algolia/instantsearch.js/commit/2c05a01)) * **pagination:** update lifecycle state ([#3979](https://github.com/algolia/instantsearch.js/issues/3979)) ([2b08344](https://github.com/algolia/instantsearch.js/commit/2b08344)) * **pagination:** update no refinement behavior ([#4124](https://github.com/algolia/instantsearch.js/issues/4124)) ([8d222ad](https://github.com/algolia/instantsearch.js/commit/8d222ad)) * **range:** clear widget state on empty refinements ([#4157](https://github.com/algolia/instantsearch.js/issues/4157)) ([23cd112](https://github.com/algolia/instantsearch.js/commit/23cd112)) * **ratingMenu:** update lifecycle state ([#3987](https://github.com/algolia/instantsearch.js/issues/3987)) ([ffadf64](https://github.com/algolia/instantsearch.js/commit/ffadf64)) * **RefinementList:** remove root css class on sublists ([#4117](https://github.com/algolia/instantsearch.js/issues/4117)) ([ceddd42](https://github.com/algolia/instantsearch.js/commit/ceddd42)), closes [/github.com/algolia/instantsearch.js/blob/v2/src/decorators/headerFooter.js#L22](https://github.com//github.com/algolia/instantsearch.js/blob/v2/src/decorators/headerFooter.js/issues/L22) * **searchBox:** update lifecycle state ([#3981](https://github.com/algolia/instantsearch.js/issues/3981)) ([0ea4950](https://github.com/algolia/instantsearch.js/commit/0ea4950)) * **sortBy:** ensure a return value for getWidgetSearchParameters ([#4126](https://github.com/algolia/instantsearch.js/issues/4126)) ([569d573](https://github.com/algolia/instantsearch.js/commit/569d573)) * **sortBy:** read initial index name from parent index ([#4079](https://github.com/algolia/instantsearch.js/issues/4079)) ([fe23c55](https://github.com/algolia/instantsearch.js/commit/fe23c55)) * display warnings only in development ([#4150](https://github.com/algolia/instantsearch.js/issues/4150)) ([44f69a0](https://github.com/algolia/instantsearch.js/commit/44f69a0)) * remove useless types ([#3958](https://github.com/algolia/instantsearch.js/issues/3958)) ([ddebf53](https://github.com/algolia/instantsearch.js/commit/ddebf53)) * **stories:** hide Places ([#4152](https://github.com/algolia/instantsearch.js/issues/4152)) ([7ff843f](https://github.com/algolia/instantsearch.js/commit/7ff843f)) * **toggleRefinement:** update lifecycle state ([#3993](https://github.com/algolia/instantsearch.js/issues/3993)) ([f1beff6](https://github.com/algolia/instantsearch.js/commit/f1beff6)) * **voiceSearch:** update lifecycle state ([#3982](https://github.com/algolia/instantsearch.js/issues/3982)) ([798e3c1](https://github.com/algolia/instantsearch.js/commit/798e3c1)) * **warnings:** remove v3 warnings ([#4134](https://github.com/algolia/instantsearch.js/issues/4134)) ([7eb6810](https://github.com/algolia/instantsearch.js/commit/7eb6810)) ### Features * **autocomplete:** leverage scoped results ([#3975](https://github.com/algolia/instantsearch.js/issues/3975)) ([8f05968](https://github.com/algolia/instantsearch.js/commit/8f05968)) * **autocomplete:** participate in routing ([#4029](https://github.com/algolia/instantsearch.js/issues/4029)) ([a9ca0c5](https://github.com/algolia/instantsearch.js/commit/a9ca0c5)) * **autocomplete:** provide indexId ([#4142](https://github.com/algolia/instantsearch.js/issues/4142)) ([b641e23](https://github.com/algolia/instantsearch.js/commit/b641e23)) * **clearRefinements:** support multiple indices ([#4036](https://github.com/algolia/instantsearch.js/issues/4036)) ([3611b11](https://github.com/algolia/instantsearch.js/commit/3611b11)) * **connectAutocomplete:** add default value on getConfiguration ([#3836](https://github.com/algolia/instantsearch.js/issues/3836)) ([724b83f](https://github.com/algolia/instantsearch.js/commit/724b83f)) * **connectAutocomplete:** clear the state on dispose ([#3815](https://github.com/algolia/instantsearch.js/issues/3815)) ([8ae87d8](https://github.com/algolia/instantsearch.js/commit/8ae87d8)) * **connectHierarchicalMenu:** update getWidgetSearchParameters ([#4053](https://github.com/algolia/instantsearch.js/issues/4053)) ([c99f822](https://github.com/algolia/instantsearch.js/commit/c99f822)) * **connectHits:** clear the state on dispose ([#3816](https://github.com/algolia/instantsearch.js/issues/3816)) ([c4de730](https://github.com/algolia/instantsearch.js/commit/c4de730)) * **connectHits:** implement getWidgetSearchParameters ([#4001](https://github.com/algolia/instantsearch.js/issues/4001)) ([c77cf66](https://github.com/algolia/instantsearch.js/commit/c77cf66)) * **connectHitsPerPage:** clear the state on dispose ([#3818](https://github.com/algolia/instantsearch.js/issues/3818)) ([d7a5c89](https://github.com/algolia/instantsearch.js/commit/d7a5c89)) * **connectInfiniteHits:** add default value on getConfiguration ([#3837](https://github.com/algolia/instantsearch.js/issues/3837)) ([8c65249](https://github.com/algolia/instantsearch.js/commit/8c65249)) * **connectInfiniteHits:** clear the state on dispose ([#3819](https://github.com/algolia/instantsearch.js/issues/3819)) ([60ce151](https://github.com/algolia/instantsearch.js/commit/60ce151)) * **connectMenu:** update getWidgetSearchParameters ([#4054](https://github.com/algolia/instantsearch.js/issues/4054)) ([7d001e7](https://github.com/algolia/instantsearch.js/commit/7d001e7)) * **connectNumericMenu:** update state lifecycle ([#4013](https://github.com/algolia/instantsearch.js/issues/4013)) ([2620c90](https://github.com/algolia/instantsearch.js/commit/2620c90)) * **connectPagination:** add default value on getConfiguration ([#3838](https://github.com/algolia/instantsearch.js/issues/3838)) ([aa4602c](https://github.com/algolia/instantsearch.js/commit/aa4602c)) * **connectPagination:** clear the state on dispose ([#3821](https://github.com/algolia/instantsearch.js/issues/3821)) ([5b8ef49](https://github.com/algolia/instantsearch.js/commit/5b8ef49)) * **connectPagination:** update getWidgetSearchParameters ([#4004](https://github.com/algolia/instantsearch.js/issues/4004)) ([eed7e77](https://github.com/algolia/instantsearch.js/commit/eed7e77)) * **connectRange:** default `precision` to 0 ([#3953](https://github.com/algolia/instantsearch.js/issues/3953)) ([632e06b](https://github.com/algolia/instantsearch.js/commit/632e06b)) * **connectRatingMenu:** update getWidgetSearchParameters ([#4008](https://github.com/algolia/instantsearch.js/issues/4008)) ([d3c96bf](https://github.com/algolia/instantsearch.js/commit/d3c96bf)) * **connectRefinementList:** update getWidgetSearchParameters ([#4010](https://github.com/algolia/instantsearch.js/issues/4010)) ([ddc8fc4](https://github.com/algolia/instantsearch.js/commit/ddc8fc4)) * **connectSearchBox:** clear the state on dispose ([#3822](https://github.com/algolia/instantsearch.js/issues/3822)) ([940522c](https://github.com/algolia/instantsearch.js/commit/940522c)) * **connectSearchBox:** mount with a default query ([#3840](https://github.com/algolia/instantsearch.js/issues/3840)) ([c3a7d69](https://github.com/algolia/instantsearch.js/commit/c3a7d69)) * **connectSearchBox:** update getWidgetSearchParameters ([#4002](https://github.com/algolia/instantsearch.js/issues/4002)) ([5c6fcd8](https://github.com/algolia/instantsearch.js/commit/5c6fcd8)) * **connectVoiceSearch:** add default value on getConfiguration ([#3841](https://github.com/algolia/instantsearch.js/issues/3841)) ([fb70363](https://github.com/algolia/instantsearch.js/commit/fb70363)) * **connectVoiceSearch:** clear the state on dispose ([#3823](https://github.com/algolia/instantsearch.js/issues/3823)) ([705b3e6](https://github.com/algolia/instantsearch.js/commit/705b3e6)) * **connectVoiceSearch:** update getWidgetSearchParameters ([#4055](https://github.com/algolia/instantsearch.js/issues/4055)) ([b8c669f](https://github.com/algolia/instantsearch.js/commit/b8c669f)) * **core:** deprecate addWidget & removeWidget ([#4131](https://github.com/algolia/instantsearch.js/issues/4131)) ([e5dafef](https://github.com/algolia/instantsearch.js/commit/e5dafef)) * **currentRefinements:** support multiple indices ([#4012](https://github.com/algolia/instantsearch.js/issues/4012)) ([e997728](https://github.com/algolia/instantsearch.js/commit/e997728)) * **defer:** implement cancellable callback ([#3916](https://github.com/algolia/instantsearch.js/issues/3916)) ([43a0bf8](https://github.com/algolia/instantsearch.js/commit/43a0bf8)) * **federated:** keep a consistent state in the RefinementList life cycle ([#3976](https://github.com/algolia/instantsearch.js/issues/3976)) ([31d0fd6](https://github.com/algolia/instantsearch.js/commit/31d0fd6)) * **hitsPerPage:** support new routing system ([#4038](https://github.com/algolia/instantsearch.js/issues/4038)) ([02502cb](https://github.com/algolia/instantsearch.js/commit/02502cb)), closes [#4069](https://github.com/algolia/instantsearch.js/issues/4069) * **index:** accept indexId ([#4070](https://github.com/algolia/instantsearch.js/issues/4070)) ([b74f8e3](https://github.com/algolia/instantsearch.js/commit/b74f8e3)) * **index:** add mergeSearchParameters function ([#3917](https://github.com/algolia/instantsearch.js/issues/3917)) ([c0fe7bb](https://github.com/algolia/instantsearch.js/commit/c0fe7bb)) * **index:** add widget ([dbbda0f](https://github.com/algolia/instantsearch.js/commit/dbbda0f)), closes [#3892](https://github.com/algolia/instantsearch.js/issues/3892) [#3893](https://github.com/algolia/instantsearch.js/issues/3893) [#3914](https://github.com/algolia/instantsearch.js/issues/3914) * **index:** compute local uiState ([#3997](https://github.com/algolia/instantsearch.js/issues/3997)) ([997c0f4](https://github.com/algolia/instantsearch.js/commit/997c0f4)) * **index:** merge `ruleContexts` search parameter ([#3944](https://github.com/algolia/instantsearch.js/issues/3944)) ([e94752d](https://github.com/algolia/instantsearch.js/commit/e94752d)) * **index:** provide scoped results to render hook ([#3964](https://github.com/algolia/instantsearch.js/issues/3964)) ([37c6aad](https://github.com/algolia/instantsearch.js/commit/37c6aad)) * **index:** replicate searchFunction hack ([#4078](https://github.com/algolia/instantsearch.js/issues/4078)) ([1d2a816](https://github.com/algolia/instantsearch.js/commit/1d2a816)), closes [/github.com/algolia/instantsearch.js/blob/509513c0feafaad522f6f18d87a441559f4aa050/src/lib/RoutingManager.ts#L113-L130](https://github.com//github.com/algolia/instantsearch.js/blob/509513c0feafaad522f6f18d87a441559f4aa050/src/lib/RoutingManager.ts/issues/L113-L130) * **index:** reset page of child indexes ([#3962](https://github.com/algolia/instantsearch.js/issues/3962)) ([131b1ce](https://github.com/algolia/instantsearch.js/commit/131b1ce)) * **index:** resolve parent SearchParameters ([#3937](https://github.com/algolia/instantsearch.js/issues/3937)) ([2611da5](https://github.com/algolia/instantsearch.js/commit/2611da5)) * **index:** use uiState driven SearchParameters ([#4059](https://github.com/algolia/instantsearch.js/issues/4059)) ([b12bb9f](https://github.com/algolia/instantsearch.js/commit/b12bb9f)) * **infiniteHits:** support new routing system ([#4040](https://github.com/algolia/instantsearch.js/issues/4040)) ([49315cf](https://github.com/algolia/instantsearch.js/commit/49315cf)) * **instantsearch:** add onStateChange method ([#4080](https://github.com/algolia/instantsearch.js/issues/4080)) ([9f68da5](https://github.com/algolia/instantsearch.js/commit/9f68da5)) * **InstantSearch:** switch to DerivedHelper only ([#3885](https://github.com/algolia/instantsearch.js/issues/3885)) ([d6fc317](https://github.com/algolia/instantsearch.js/commit/d6fc317)) * **places:** add Places widget ([#4167](https://github.com/algolia/instantsearch.js/issues/4167)) ([1d754d1](https://github.com/algolia/instantsearch.js/commit/1d754d1)) * drop support of searchParameters for initialUiState ([#4081](https://github.com/algolia/instantsearch.js/issues/4081)) ([571efeb](https://github.com/algolia/instantsearch.js/commit/571efeb)) * **range:** support new routing system ([#4039](https://github.com/algolia/instantsearch.js/issues/4039)) ([8cba05a](https://github.com/algolia/instantsearch.js/commit/8cba05a)) * **routing:** add a "single index" compatibility mode ([#4087](https://github.com/algolia/instantsearch.js/issues/4087)) ([842eb0f](https://github.com/algolia/instantsearch.js/commit/842eb0f)) * **RoutingManager:** update state on route update ([#4100](https://github.com/algolia/instantsearch.js/issues/4100)) ([88f2615](https://github.com/algolia/instantsearch.js/commit/88f2615)) * **toggleRefinement:** support new routing system ([#4037](https://github.com/algolia/instantsearch.js/issues/4037)) ([6a9d99f](https://github.com/algolia/instantsearch.js/commit/6a9d99f)) * **types:** DerivedHelper ([#3887](https://github.com/algolia/instantsearch.js/issues/3887)) ([0f38b4a](https://github.com/algolia/instantsearch.js/commit/0f38b4a)) * **types:** rename RenderOptions -> RendererOptions ([#3867](https://github.com/algolia/instantsearch.js/issues/3867)) ([05c6f72](https://github.com/algolia/instantsearch.js/commit/05c6f72)) * **utils:** implement defer ([#3882](https://github.com/algolia/instantsearch.js/issues/3882)) ([8af470e](https://github.com/algolia/instantsearch.js/commit/8af470e)) * **voice:** add additional query parameters ([#3738](https://github.com/algolia/instantsearch.js/issues/3738)) ([c555255](https://github.com/algolia/instantsearch.js/commit/c555255)) * drop suppot for onHistoryChange ([#3941](https://github.com/algolia/instantsearch.js/issues/3941)) ([697f609](https://github.com/algolia/instantsearch.js/commit/697f609)) * introduce initialUiState option ([#4074](https://github.com/algolia/instantsearch.js/issues/4074)) ([de00707](https://github.com/algolia/instantsearch.js/commit/de00707)) * update UiState definition ([#4075](https://github.com/algolia/instantsearch.js/issues/4075)) ([9e7d3d8](https://github.com/algolia/instantsearch.js/commit/9e7d3d8)) * **widgets:** add `$$type` to widgets definition ([#3960](https://github.com/algolia/instantsearch.js/issues/3960)) ([344d1b7](https://github.com/algolia/instantsearch.js/commit/344d1b7)) # [3.7.0](https://github.com/algolia/instantsearch.js/compare/v3.5.4...v3.7.0) (2019-10-08) ### Bug Fixes * **clearRefinements:** reset page to 0 ([#3936](https://github.com/algolia/instantsearch.js/issues/3936)) ([7378a0a](https://github.com/algolia/instantsearch.js/commit/7378a0a)) * **connectSortBy:** never update the initial index ([#4015](https://github.com/algolia/instantsearch.js/issues/4015)) ([bc0f9e2](https://github.com/algolia/instantsearch.js/commit/bc0f9e2)) * **deps:** update dependency instantsearch.js to v3.5.4 ([#3929](https://github.com/algolia/instantsearch.js/issues/3929)) ([eff84c5](https://github.com/algolia/instantsearch.js/commit/eff84c5)) * **deps:** update dependency instantsearch.js to v3.6.0 ([#4021](https://github.com/algolia/instantsearch.js/issues/4021)) ([7719bba](https://github.com/algolia/instantsearch.js/commit/7719bba)) * **enhanceConfiguration:** deduplicate the hierarchicalFacets ([#3966](https://github.com/algolia/instantsearch.js/issues/3966)) ([baf8a35](https://github.com/algolia/instantsearch.js/commit/baf8a35)) * **examples:** fix IE11 compatibility for e-commerce demo ([#4049](https://github.com/algolia/instantsearch.js/issues/4049)) ([dc6f350](https://github.com/algolia/instantsearch.js/commit/dc6f350)) * **examples:** fix missing polyfill in e-commerce demo ([#4076](https://github.com/algolia/instantsearch.js/issues/4076)) ([4bf3ab3](https://github.com/algolia/instantsearch.js/commit/4bf3ab3)) * **hierarchicalFacets:** prevent different rootPath on same attribute ([#3965](https://github.com/algolia/instantsearch.js/issues/3965)) ([5ee79fa](https://github.com/algolia/instantsearch.js/commit/5ee79fa)) * **instantsearch:** warn deprecated usage of `searchParameters` ([#4151](https://github.com/algolia/instantsearch.js/issues/4151)) ([18e1c36](https://github.com/algolia/instantsearch.js/commit/18e1c36)) * **menuSelect:** unmount component ([#3911](https://github.com/algolia/instantsearch.js/issues/3911)) ([f6debce](https://github.com/algolia/instantsearch.js/commit/f6debce)) * **rangeInput:** unmount component ([#3910](https://github.com/algolia/instantsearch.js/issues/3910)) ([f6c29e8](https://github.com/algolia/instantsearch.js/commit/f6c29e8)) * **refinementList:** fix showMore button to work after search ([#3082](https://github.com/algolia/instantsearch.js/issues/3082)) ([23e46b6](https://github.com/algolia/instantsearch.js/commit/23e46b6)) * pass noop as default value to unmountFn at connectors ([#3955](https://github.com/algolia/instantsearch.js/issues/3955)) ([7c38744](https://github.com/algolia/instantsearch.js/commit/7c38744)) # [3.6.0](https://github.com/algolia/instantsearch.js/compare/v3.5.4...v3.6.0) (2019-07-30) ### Bug Fixes * **clearRefinements:** reset page to 0 ([#3936](https://github.com/algolia/instantsearch.js/issues/3936)) ([7378a0a](https://github.com/algolia/instantsearch.js/commit/7378a0a)) * pass noop as default value to unmountFn at connectors ([#3955](https://github.com/algolia/instantsearch.js/issues/3955)) ([7c38744](https://github.com/algolia/instantsearch.js/commit/7c38744)) * **enhanceConfiguration:** deduplicate the hierarchicalFacets ([#3966](https://github.com/algolia/instantsearch.js/issues/3966)) ([baf8a35](https://github.com/algolia/instantsearch.js/commit/baf8a35)) * **hierarchicalFacets:** prevent different rootPath on same attribute ([#3965](https://github.com/algolia/instantsearch.js/issues/3965)) ([5ee79fa](https://github.com/algolia/instantsearch.js/commit/5ee79fa)) * **menuSelect:** unmount component ([#3911](https://github.com/algolia/instantsearch.js/issues/3911)) ([f6debce](https://github.com/algolia/instantsearch.js/commit/f6debce)) * **rangeInput:** unmount component ([#3910](https://github.com/algolia/instantsearch.js/issues/3910)) ([f6c29e8](https://github.com/algolia/instantsearch.js/commit/f6c29e8)) * **refinementList:** fix showMore button to work after search ([#3082](https://github.com/algolia/instantsearch.js/issues/3082)) ([23e46b6](https://github.com/algolia/instantsearch.js/commit/23e46b6)) ## [3.5.4](https://github.com/algolia/instantsearch.js/compare/v3.5.3...v3.5.4) (2019-07-01) ### Bug Fixes * **connectSortBy:** do not throw with wrong indexes ([#3824](https://github.com/algolia/instantsearch.js/issues/3824)) ([2a84ee2](https://github.com/algolia/instantsearch.js/commit/2a84ee2)) * **deps:** update dependency instantsearch.js to v3.5.3 ([#3877](https://github.com/algolia/instantsearch.js/issues/3877)) ([463f3bb](https://github.com/algolia/instantsearch.js/commit/463f3bb)) * **escape:** make sure that __escaped does not get removed ([#3830](https://github.com/algolia/instantsearch.js/issues/3830)) ([fbafd22](https://github.com/algolia/instantsearch.js/commit/fbafd22)) * **getRefinements:** check for facet before accessing its data ([#3842](https://github.com/algolia/instantsearch.js/issues/3842)) ([aadc769](https://github.com/algolia/instantsearch.js/commit/aadc769)) * **panel:** return value from dispose ([#3895](https://github.com/algolia/instantsearch.js/issues/3895)) ([bceb78f](https://github.com/algolia/instantsearch.js/commit/bceb78f)) * **voiceSearch:** remove event listeners on stop ([#3845](https://github.com/algolia/instantsearch.js/issues/3845)) ([688e36a](https://github.com/algolia/instantsearch.js/commit/688e36a)) ## [3.5.3](https://github.com/algolia/instantsearch.js/compare/v3.5.1...v3.5.3) (2019-05-28) ### Bug Fixes * **voiceSearch:** let the connector handle the default value of searchAsYouSpeak when it's not given ([#3817](https://github.com/algolia/instantsearch.js/issues/3817)) ([9d3e91b](https://github.com/algolia/instantsearch.js/commit/9d3e91b)) * **getTag:** use object version of toString ([#3820](https://github.com/algolia/instantsearch.js/issues/3820)) ([a7348ea](https://github.com/algolia/instantsearch.js/commit/a7348ea)) * **types:** fix cssClasses of voiceSearch ([#3783](https://github.com/algolia/instantsearch.js/issues/3783)) ([f016326](https://github.com/algolia/instantsearch.js/commit/f016326)) # [3.5.1](https://github.com/algolia/instantsearch.js/compare/v3.4.0...v3.5.1) (2019-05-20) ### Bug Fixes * **types:** improve types for voiceSearch ([#3778](https://github.com/algolia/instantsearch.js/issues/3778)) ([ed2d61a](https://github.com/algolia/instantsearch.js/commit/ed2d61a)) * **types:** update UiState type ([#3777](https://github.com/algolia/instantsearch.js/issues/3777)) ([36e3a3d](https://github.com/algolia/instantsearch.js/commit/36e3a3d)) * **voiceSearch:** remove event listeners on dispose ([#3779](https://github.com/algolia/instantsearch.js/issues/3779)) ([0e988cc](https://github.com/algolia/instantsearch.js/commit/0e988cc)) * **hitsPerPage:** improve warning for missing state value ([#3707](https://github.com/algolia/instantsearch.js/issues/3707)) ([93d8432](https://github.com/algolia/instantsearch.js/commit/93d8432)) * **numericMenu:** prevent refinement reset on checked radio click ([#3749](https://github.com/algolia/instantsearch.js/issues/3749)) ([e4a6e75](https://github.com/algolia/instantsearch.js/commit/e4a6e75)) * **rangeSlider:** round the slider pit value ([#3758](https://github.com/algolia/instantsearch.js/issues/3758)) ([6edee3e](https://github.com/algolia/instantsearch.js/commit/6edee3e)), closes [#2904](https://github.com/algolia/instantsearch.js/issues/2904) * **types:** improve UiState types ([#3763](https://github.com/algolia/instantsearch.js/issues/3763)) ([e8ea57b](https://github.com/algolia/instantsearch.js/commit/e8ea57b)) * **voice:** import correct noop ([#3766](https://github.com/algolia/instantsearch.js/issues/3766)) ([6a80422](https://github.com/algolia/instantsearch.js/commit/6a80422)) ### Features * **voiceSearch:** add connector and widget ([#3601](https://github.com/algolia/instantsearch.js/issues/3601)) ([21e4d81](https://github.com/algolia/instantsearch.js/commit/21e4d81)) ### Reverts * chore(build): remove PropTypes from builds ([#3697](https://github.com/algolia/instantsearch.js/issues/3697)) ([#3776](https://github.com/algolia/instantsearch.js/issues/3776)) ([1e6be79](https://github.com/algolia/instantsearch.js/commit/1e6be79)) # [3.4.0](https://github.com/algolia/instantsearch.js/compare/v3.3.0...v3.4.0) (2019-04-17) ### Bug Fixes * **storybook:** fix Hierarchical menu separator in Breadcrumb story ([#3695](https://github.com/algolia/instantsearch.js/issues/3695)) ([b3bf8ac](https://github.com/algolia/instantsearch.js/commit/b3bf8ac)) * **tools:** use commonjs in bump-package-version.js ([#3699](https://github.com/algolia/instantsearch.js/issues/3699)) ([6a6dbe1](https://github.com/algolia/instantsearch.js/commit/6a6dbe1)) * **types:** fix wrong typing in getWidgetState ([#3693](https://github.com/algolia/instantsearch.js/issues/3693)) ([b3c2154](https://github.com/algolia/instantsearch.js/commit/b3c2154)) * **types:** remove unused Without type ([#3694](https://github.com/algolia/instantsearch.js/issues/3694)) ([656d000](https://github.com/algolia/instantsearch.js/commit/656d000)) ### Features * **infiniteHits:** add previous button ([#3675](https://github.com/algolia/instantsearch.js/issues/3675)) ([2e6137b](https://github.com/algolia/instantsearch.js/commit/2e6137b)) * **Insights:** Insights inside Instantsearch ([#3598](https://github.com/algolia/instantsearch.js/issues/3598)) ([387f41f](https://github.com/algolia/instantsearch.js/commit/387f41f)) # [3.3.0](https://github.com/algolia/instantsearch.js/compare/v3.2.1...v3.3.0) (2019-04-11) ### Bug Fixes * **connectQueryRules:** improve tracked refinement type ([#3648](https://github.com/algolia/instantsearch.js/issues/3648)) ([e16ad57](https://github.com/algolia/instantsearch.js/commit/e16ad57)) * **currentRefinements:** don't rely on `_objectSpread` ([#3672](https://github.com/algolia/instantsearch.js/issues/3672)) ([cd64bcf](https://github.com/algolia/instantsearch.js/commit/cd64bcf)) * **queryRuleCustomData:** add default template ([#3650](https://github.com/algolia/instantsearch.js/issues/3650)) ([83e9eaa](https://github.com/algolia/instantsearch.js/commit/83e9eaa)) * **QueryRuleCustomData:** pass data as object to templates ([#3647](https://github.com/algolia/instantsearch.js/issues/3647)) ([b8f8b4e](https://github.com/algolia/instantsearch.js/commit/b8f8b4e)) * **queryRules:** fix types and stories ([#3670](https://github.com/algolia/instantsearch.js/issues/3670)) ([ba6e2e6](https://github.com/algolia/instantsearch.js/commit/ba6e2e6)) * **routing:** apply windowTitle on first load ([#3669](https://github.com/algolia/instantsearch.js/issues/3669)) ([d553502](https://github.com/algolia/instantsearch.js/commit/d553502)), closes [#3667](https://github.com/algolia/instantsearch.js/issues/3667) * **routing:** support parsing URLs with up to 100 refinements ([#3671](https://github.com/algolia/instantsearch.js/issues/3671)) ([6ddcfb6](https://github.com/algolia/instantsearch.js/commit/6ddcfb6)) * **RoutingManager:** avoid stale uiState ([#3630](https://github.com/algolia/instantsearch.js/issues/3630)) ([e1588aa](https://github.com/algolia/instantsearch.js/commit/e1588aa)) * **types:** improve InstantSearch types ([#3651](https://github.com/algolia/instantsearch.js/issues/3651)) ([db9b91e](https://github.com/algolia/instantsearch.js/commit/db9b91e)) * **ua:** Update the User-Agent to use the new format ([#3616](https://github.com/algolia/instantsearch.js/issues/3616)) ([ab84c57](https://github.com/algolia/instantsearch.js/commit/ab84c57)) ### Features * **infiniteHits:** add previous button ([#3645](https://github.com/algolia/instantsearch.js/issues/3645)) ([2c9e38d](https://github.com/algolia/instantsearch.js/commit/2c9e38d)) * **queryRules:** add connectQueryRules connector ([#3597](https://github.com/algolia/instantsearch.js/issues/3597)) ([924cd99](https://github.com/algolia/instantsearch.js/commit/924cd99)), closes [#3599](https://github.com/algolia/instantsearch.js/issues/3599) [#3600](https://github.com/algolia/instantsearch.js/issues/3600) * **queryRules:** add context features to Query Rules ([#3617](https://github.com/algolia/instantsearch.js/issues/3617)) ([922879e](https://github.com/algolia/instantsearch.js/commit/922879e)), closes [#3602](https://github.com/algolia/instantsearch.js/issues/3602) ### Reverts * feat(infiniteHits): add previous button ([214c0fc](https://github.com/algolia/instantsearch.js/commit/214c0fc)) ## [3.2.1](https://github.com/algolia/instantsearch.js/compare/v3.1.0...v3.2.1) (2019-03-18) ### Bug Fixes * **connectToggleRefinement:** keep user provided, but falsy values ([#3526](https://github.com/algolia/instantsearch.js/issues/3526)) ([958a151](https://github.com/algolia/instantsearch.js/commit/958a151)) * **instantsearch:** update usage errors ([#3543](https://github.com/algolia/instantsearch.js/issues/3543)) ([a2a800b](https://github.com/algolia/instantsearch.js/commit/a2a800b)) * **panel:** append panel body as a child element ([#3561](https://github.com/algolia/instantsearch.js/issues/3561)) ([3de59a3](https://github.com/algolia/instantsearch.js/commit/3de59a3)) * **poweredBy:** remove TypeScript extension in import ([#3530](https://github.com/algolia/instantsearch.js/issues/3530)) ([99ecc0b](https://github.com/algolia/instantsearch.js/commit/99ecc0b)), closes [#3528](https://github.com/algolia/instantsearch.js/issues/3528) * **release:** update doctoc script ([e07c654](https://github.com/algolia/instantsearch.js/commit/e07c654)) * **searchbox:** unmount component on dispose ([#3563](https://github.com/algolia/instantsearch.js/issues/3563)) ([c3f0435](https://github.com/algolia/instantsearch.js/commit/c3f0435)) * **searchBox:** add reusable SearchBox component ([#3489](https://github.com/algolia/instantsearch.js/issues/3489)) ([c073a9a](https://github.com/algolia/instantsearch.js/commit/c073a9a)) ### Features * **panel:** implement collapsed feature ([#3575](https://github.com/algolia/instantsearch.js/issues/3575)) ([e84b02b](https://github.com/algolia/instantsearch.js/commit/e84b02b)) # [3.2.0](https://github.com/algolia/instantsearch.js/compare/v3.1.0...v3.2.0) (2019-03-14) ### Bug Fixes * **instantsearch:** update usage errors ([#3543](https://github.com/algolia/instantsearch.js/issues/3543)) ([a2a800b](https://github.com/algolia/instantsearch.js/commit/a2a800b)) * **searchBox:** add reusable SearchBox component ([#3489](https://github.com/algolia/instantsearch.js/issues/3489)) ([c073a9a](https://github.com/algolia/instantsearch.js/commit/c073a9a)) ### Features * **panel:** implement collapsed feature ([#3575](https://github.com/algolia/instantsearch.js/issues/3575)) ([e84b02b](https://github.com/algolia/instantsearch.js/commit/e84b02b))
## [3.1.1](https://github.com/algolia/instantsearch.js/compare/v3.1.0...v3.1.1) (2019-02-14) ### Bug Fixes * **connectToggleRefinement:** keep user provided, but falsy values ([#3526](https://github.com/algolia/instantsearch.js/issues/3526)) ([958a151](https://github.com/algolia/instantsearch.js/commit/958a151)) * **poweredBy:** remove TypeScript extension in import ([#3530](https://github.com/algolia/instantsearch.js/issues/3530)) ([99ecc0b](https://github.com/algolia/instantsearch.js/commit/99ecc0b)), closes [#3528](https://github.com/algolia/instantsearch.js/issues/3528) * **release:** update doctoc script ([e07c654](https://github.com/algolia/instantsearch.js/commit/e07c654)) ## [3.1.0](https://github.com/algolia/instantsearch.js/compare/v3.0.0...v3.1.0) (2019-02-13) ### Features * **connectCurrentRefinements**: add a root label ([#3515](https://github.com/algolia/instantsearch.js/pull/3515)) ([b8f774f](https://github.com/algolia/instantsearch.js/commit/b8f774f)) * Update error messages ([#3516](https://github.com/algolia/instantsearch.js/pull/3516)) * **InstantSearch**: remove event listeners on dispose ([#3420](https://github.com/algolia/instantsearch.js/pull/3420)) * **InstantSearch**: set helper to `null` on dispose ([#3415](https://github.com/algolia/instantsearch.js/pull/3415)) * **utils**: warn only in development ([#3367](https://github.com/algolia/instantsearch.js/pull/3367)) ### Bug Fixes * **InstantSearch**: set helper to `null` on dispose ([#3415](https://github.com/algolia/instantsearch.js/pull/3415)) * **utils**: warn only in development ([#3367](https://github.com/algolia/instantsearch.js/pull/3367)) ## [3.0.0](https://github.com/algolia/instantsearch.js/compare/v2.10.3...v3.0.0) (2018-12-20) Check the [migration guide](https://github.com/algolia/instantsearch.js/blob/879aa20d3c1e2fe906bc526b05c57f6847c433be/docgen/src/guides/v3-migration.md). ## [2.10.4](https://github.com/algolia/instantsearch.js/compare/v2.10.3...v2.10.4) (2018-10-30) ### Bug Fixes * **getRefinements:** provide attributeName for type: query ([6a58b99](https://github.com/algolia/instantsearch.js/commit/6a58b99)), closes [#3205](https://github.com/algolia/instantsearch.js/issues/3205) ## [2.10.3](https://github.com/algolia/instantsearch.js/compare/v2.10.2...v2.10.3) (2018-10-29) ### Bug Fixes * **deps:** unpin production dependencies ([257ecb7](https://github.com/algolia/instantsearch.js/commit/257ecb7)) * **InstantSearch:** avoid useless search on addWidgets ([#3178](https://github.com/algolia/instantsearch.js/issues/3178)) ([961626d](https://github.com/algolia/instantsearch.js/commit/961626d)) * **numericselector:** default value can be undefined ([#3139](https://github.com/algolia/instantsearch.js/issues/3139)) ([39d22f5](https://github.com/algolia/instantsearch.js/commit/39d22f5)) ### Features * **utils:** add warn function ([#3147](https://github.com/algolia/instantsearch.js/issues/3147)) ([9de87bb](https://github.com/algolia/instantsearch.js/commit/9de87bb)) ## [2.10.2](https://github.com/algolia/instantsearch.js/compare/v2.10.1...v2.10.2) (2018-09-10) ### Bug Fixes * **searchbox:** Add missing color to searchbox input field ([#3086](https://github.com/algolia/instantsearch.js/issues/3086)) ([62b852a](https://github.com/algolia/instantsearch.js/commit/62b852a)), closes [#3075](https://github.com/algolia/instantsearch.js/issues/3075) * **Stats:** let the widget render on all values ([#3070](https://github.com/algolia/instantsearch.js/issues/3070)) ([cd8f17e](https://github.com/algolia/instantsearch.js/commit/cd8f17e)), closes [#3056](https://github.com/algolia/instantsearch.js/issues/3056) ## [2.10.1](https://github.com/algolia/instantsearch.js/compare/v2.10.0...v2.10.1) (2018-08-17) ### Bug Fixes * **connectBreadcrumb:** ensure that data is an array ([#3067](https://github.com/algolia/instantsearch.js/issues/3067)) ([759f709](https://github.com/algolia/instantsearch.js/commit/759f709)) # [2.10.0](https://github.com/algolia/instantsearch.js/compare/v2.9.0...v2.10.0) (2018-08-08) ### Bug Fixes * **release:** provide interactive TTY for npm publish ([#3053](https://github.com/algolia/instantsearch.js/issues/3053)) ([ede9460](https://github.com/algolia/instantsearch.js/commit/ede9460)) ### Features * Implement `transformItems` API ([#3042](https://github.com/algolia/instantsearch.js/issues/3042)) ([1510a94](https://github.com/algolia/instantsearch.js/commit/1510a94)) # [2.9.0](https://github.com/algolia/instantsearch.js/compare/v2.8.1...v2.9.0) (2018-07-18) ### Features * **infiniteHits:** add showmoreButton to cssClasses ([#3026](https://github.com/algolia/instantsearch.js/issues/3026)) ([8287de0](https://github.com/algolia/instantsearch.js/commit/8287de0)) ## [2.8.1](https://github.com/algolia/instantsearch.js/compare/v2.8.0...v2.8.1) (2018-07-03) ### Bug Fixes * **connectHitsPerPage:** default value should not break the API ([#3006](https://github.com/algolia/instantsearch.js/issues/3006)) ([6635304](https://github.com/algolia/instantsearch.js/commit/6635304)), closes [#2732](https://github.com/algolia/instantsearch.js/issues/2732) * **connectRefinementList:** throw error with usage ([#2962](https://github.com/algolia/instantsearch.js/issues/2962)) ([f60222d](https://github.com/algolia/instantsearch.js/commit/f60222d)) * **sourcemap:** provide good url ([#3011](https://github.com/algolia/instantsearch.js/issues/3011)) ([9632ade](https://github.com/algolia/instantsearch.js/commit/9632ade)) * **warning:** make sure suggested import is possible ([#3014](https://github.com/algolia/instantsearch.js/issues/3014)) ([eb27152](https://github.com/algolia/instantsearch.js/commit/eb27152)) # [2.8.0](https://github.com/algolia/instantsearch.js/compare/v2.7.6...v2.8.0) (2018-05-30) ### Features * **connectors:** add connectAutocomplete ([#2841](https://github.com/algolia/instantsearch.js/issues/2841)) ([4bec81e](https://github.com/algolia/instantsearch.js/commit/4bec81e)), closes [/github.com/algolia/instantsearch.js/pull/2841#discussion_r188383882](https://github.com//github.com/algolia/instantsearch.js/pull/2841/issues/discussion_r188383882) [#2313](https://github.com/algolia/instantsearch.js/issues/2313) * **search-client:** Add support for Universal Search Clients ([#2894](https://github.com/algolia/instantsearch.js/issues/2894)) ([5df3c74](https://github.com/algolia/instantsearch.js/commit/5df3c74)), closes [#2905](https://github.com/algolia/instantsearch.js/issues/2905) ## [2.7.6](https://github.com/algolia/instantsearch.js/compare/v2.7.5...v2.7.6) (2018-05-29) ### Bug Fixes * **connectConfigure:** ensure we do not extend `SearchParameters` ([#2945](https://github.com/algolia/instantsearch.js/issues/2945)) ([fdb4a7a](https://github.com/algolia/instantsearch.js/commit/fdb4a7a)) * **infinite-hits:** fix [#2543](https://github.com/algolia/instantsearch.js/issues/2543) ([#2948](https://github.com/algolia/instantsearch.js/issues/2948)) ([bbf9f8f](https://github.com/algolia/instantsearch.js/commit/bbf9f8f)) ## [2.7.5](https://github.com/algolia/instantsearch.js/compare/v2.7.4...v2.7.5) (2018-05-28) ### Bug Fixes * **clear-all:** apply excludeAttribute correctly with clearsQuery ([#2935](https://github.com/algolia/instantsearch.js/issues/2935)) ([e782ab8](https://github.com/algolia/instantsearch.js/commit/e782ab8)) * **connectInfiniteHits:** fix [#2928](https://github.com/algolia/instantsearch.js/issues/2928) ([#2939](https://github.com/algolia/instantsearch.js/issues/2939)) ([0293a31](https://github.com/algolia/instantsearch.js/commit/0293a31)) ## [2.7.4](https://github.com/algolia/instantsearch.js/compare/v2.7.3...v2.7.4) (2018-05-03) ### Bug Fixes * **searchFunction:** Fix unresolved returned Promise ([#2913](https://github.com/algolia/instantsearch.js/issues/2913)) ([5286c7c](https://github.com/algolia/instantsearch.js/commit/5286c7c)) ## [2.7.3](https://github.com/algolia/instantsearch.js/compare/v2.7.2...v2.7.3) (2018-04-26) ### Bug Fixes * **index.es6:** avoid use of Object.assign for IE ([#2908](https://github.com/algolia/instantsearch.js/issues/2908)) ([228b02e](https://github.com/algolia/instantsearch.js/commit/228b02e)) ## [2.7.2](https://github.com/algolia/instantsearch.js/compare/v2.7.1...v2.7.2) (2018-04-18) ### Bug Fixes * **routing:** should apply stateMapping when doing initial write ([#2892](https://github.com/algolia/instantsearch.js/issues/2892)) ([7f62e6dc](https://github.com/algolia/instantsearch.js/commit/7f62e6dc)) * **ie:** do not rely on Object.assign ([#2885](https://github.com/algolia/instantsearch.js/issues/2885)) ([88497e56](https://github.com/algolia/instantsearch.js/commit/88497e56)) ## [2.7.1](https://github.com/algolia/instantsearch.js/compare/v2.7.0...v2.7.1) (2018-04-11) ### Bug Fixes * **history:** provide location and use named parameters ([#2877](https://github.com/algolia/instantsearch.js/issues/2877)) ([761ffa4](https://github.com/algolia/instantsearch.js/commit/761ffa4)) # [2.7.0](https://github.com/algolia/instantsearch.js/compare/v2.6.3...v2.7.0) (2018-04-09) ### Bug Fixes * pagination padding ([#2866](https://github.com/algolia/instantsearch.js/issues/2866)) ([e8c58cc](https://github.com/algolia/instantsearch.js/commit/e8c58cc)) * **geosearch:** avoid reset map when it already moved ([#2870](https://github.com/algolia/instantsearch.js/issues/2870)) ([f171b8a](https://github.com/algolia/instantsearch.js/commit/f171b8a)) * **removeWidget:** check for widgets.length on next tick ([#2831](https://github.com/algolia/instantsearch.js/issues/2831)) ([7e639d6](https://github.com/algolia/instantsearch.js/commit/7e639d6)) ### Features * **connetConfigure:** add a connector to create a connector widget ([8fdf752](https://github.com/algolia/instantsearch.js/commit/8fdf752)) * **routing:** provide a mechanism to synchronize the search ([#2829](https://github.com/algolia/instantsearch.js/issues/2829)) ([75b2ca3](https://github.com/algolia/instantsearch.js/commit/75b2ca3)), closes [#2849](https://github.com/algolia/instantsearch.js/issues/2849) [#2849](https://github.com/algolia/instantsearch.js/issues/2849) * **size:** add sideEffects false to package.json ([#2861](https://github.com/algolia/instantsearch.js/issues/2861)) ([f5d1ab1](https://github.com/algolia/instantsearch.js/commit/f5d1ab1)), closes [#2859](https://github.com/algolia/instantsearch.js/issues/2859) ## [2.6.3](https://github.com/algolia/instantsearch.js/compare/v2.6.2...v2.6.3) (2018-03-30) ### Bug Fixes * **rangeSlider:** handles were blocked ([#2849](https://github.com/algolia/instantsearch.js/issues/2849)) ([a2af4f0](https://github.com/algolia/instantsearch.js/commit/a2af4f0)) ## [2.6.2](https://github.com/algolia/instantsearch.js/compare/v2.6.1...v2.6.2) (2018-03-29) ### Bug Fixes * **connectGeoSearch:** correctly dispose the connector ([#2845](https://github.com/algolia/instantsearch.js/issues/2845)) ([a4eafd2](https://github.com/algolia/instantsearch.js/commit/a4eafd2)) * **GeoSearch:** correctly unmount the widget ([#2846](https://github.com/algolia/instantsearch.js/issues/2846)) ([f31ef3c](https://github.com/algolia/instantsearch.js/commit/f31ef3c)) ## [2.6.1](https://github.com/algolia/instantsearch.js/compare/v2.6.0...v2.6.1) (2018-03-28) ### Bug Fixes * **connectBreadcrumb:** allow unmounting ([#2815](https://github.com/algolia/instantsearch.js/issues/2815)) ([c6c353a](https://github.com/algolia/instantsearch.js/commit/c6c353a)) * **connectBreadcrumb:** update typo in property type items ([#2782](https://github.com/algolia/instantsearch.js/issues/2782)) ([79ebd66](https://github.com/algolia/instantsearch.js/commit/79ebd66)) * **docgen:** pass the relatedTypes to the struct mixin in connectors layout ([#2780](https://github.com/algolia/instantsearch.js/issues/2780)) ([f7f8b05](https://github.com/algolia/instantsearch.js/commit/f7f8b05)) * **GeoSearch:** update typo in property type cssClasses ([#2781](https://github.com/algolia/instantsearch.js/issues/2781)) ([419c2ab](https://github.com/algolia/instantsearch.js/commit/419c2ab)) * **main:** correctly import EventEmitter ([#2814](https://github.com/algolia/instantsearch.js/issues/2814)) ([8fa3649](https://github.com/algolia/instantsearch.js/commit/8fa3649)), closes [#2730](https://github.com/algolia/instantsearch.js/issues/2730) # [2.6.0](https://github.com/algolia/instantsearch.js/compare/v2.5.2...v2.6.0) (2018-03-06) ### Bug Fixes * **GeoSearch:** add apiKey for Google Maps ([#2773](https://github.com/algolia/instantsearch.js/issues/2773)) ([6c1846f](https://github.com/algolia/instantsearch.js/commit/6c1846f)) * **GeoSearch:** override button style ([#2772](https://github.com/algolia/instantsearch.js/issues/2772)) ([4d69b50](https://github.com/algolia/instantsearch.js/commit/4d69b50)) ### Features * **configure:** add the Configure widget ([#2698](https://github.com/algolia/instantsearch.js/issues/2698)) ([94daabc](https://github.com/algolia/instantsearch.js/commit/94daabc)) * add GeoSearch widget & connector ([#2743](https://github.com/algolia/instantsearch.js/issues/2743)) ([7fa17ff](https://github.com/algolia/instantsearch.js/commit/7fa17ff)) ## [2.5.2](https://github.com/algolia/instantsearch.js/compare/v2.5.1...v2.5.2) (2018-02-26) ### Bug Fixes * **Template:** harden Symbol checks ([#2749](https://github.com/algolia/instantsearch.js/issues/2749)) ([fab66bc](https://github.com/algolia/instantsearch.js/commit/fab66bc)) * **yarnrc:** use empty string for save-prefix ([#2739](https://github.com/algolia/instantsearch.js/issues/2739)) ([979e0cd](https://github.com/algolia/instantsearch.js/commit/979e0cd)) ## [2.5.1](https://github.com/algolia/instantsearch.js/compare/v2.5.0...v2.5.1) (2018-02-13) ### Bug Fixes * **perf:** only compute snappoints when step is provided ([#2699](https://github.com/algolia/instantsearch.js/issues/2699)) ([ce9ca19](https://github.com/algolia/instantsearch.js/commit/ce9ca19)), closes [#2662](https://github.com/algolia/instantsearch.js/issues/2662) # [2.5.0](https://github.com/algolia/instantsearch.js/compare/v2.4.1...v2.5.0) (2018-02-06) ### Bug Fixes * **doc:** add maximum width to images (fix [#2685](https://github.com/algolia/instantsearch.js/issues/2685)) ([#2686](https://github.com/algolia/instantsearch.js/issues/2686)) ([f4b5377](https://github.com/algolia/instantsearch.js/commit/f4b5377)) ### Features * support for algolia insights ([#2689](https://github.com/algolia/instantsearch.js/issues/2689)) ([96b8d61](https://github.com/algolia/instantsearch.js/commit/96b8d61)) ## [2.4.1](https://github.com/algolia/instantsearch.js/compare/v2.4.0...v2.4.1) (2018-01-04) ### Bug Fixes * **core:** correct escape highlight for arrays and nested objects ([#2646](https://github.com/algolia/instantsearch.js/issues/2646)) ([ed0ee73](https://github.com/algolia/instantsearch.js/commit/ed0ee73)) # [2.4.0](https://github.com/algolia/instantsearch.js/compare/v2.3.3...v2.4.0) (2018-01-02) ### Bug Fixes * **pagination:** disable buttons if not results ([#2643](https://github.com/algolia/instantsearch.js/issues/2643)) ([9017b72](https://github.com/algolia/instantsearch.js/commit/9017b72)), closes [#2014](https://github.com/algolia/instantsearch.js/issues/2014) * **theme:** fix height of pagination ([#2641](https://github.com/algolia/instantsearch.js/issues/2641)) ([b3185e5](https://github.com/algolia/instantsearch.js/commit/b3185e5)) ### Features * **core:** add a reload method on the InstantSearch component ([#2637](https://github.com/algolia/instantsearch.js/issues/2637)) ([e73ff13](https://github.com/algolia/instantsearch.js/commit/e73ff13)) * **core:** add an error event to monitor error from Algolia ([#2642](https://github.com/algolia/instantsearch.js/issues/2642)) ([71c2d68](https://github.com/algolia/instantsearch.js/commit/71c2d68)), closes [#1585](https://github.com/algolia/instantsearch.js/issues/1585) * **core:** rename `reload` to `refresh` ([#2645](https://github.com/algolia/instantsearch.js/issues/2645)) ([9b8ac65](https://github.com/algolia/instantsearch.js/commit/9b8ac65)) * **wrapWithHits:** enable async init ([#2635](https://github.com/algolia/instantsearch.js/issues/2635)) ([08a8747](https://github.com/algolia/instantsearch.js/commit/08a8747)) ## [2.3.3](https://github.com/algolia/instantsearch.js/compare/v2.3.2...v2.3.3) (2017-12-11) ### Bug Fixes * **core:** search is stalled at init ([#2623](https://github.com/algolia/instantsearch.js/issues/2623)) ([e3dd577](https://github.com/algolia/instantsearch.js/commit/e3dd577)), closes [#2616](https://github.com/algolia/instantsearch.js/issues/2616) ## [2.3.2](https://github.com/algolia/instantsearch.js/compare/v2.3.1...v2.3.2) (2017-12-06) ### Bug Fixes * React reference: Breadcrumb & RangeInput components ([#2618](https://github.com/algolia/instantsearch.js/issues/2618)) ([7f32161](https://github.com/algolia/instantsearch.js/commit/7f32161)) ## [2.3.1](https://github.com/algolia/instantsearch.js/compare/v2.3.0...v2.3.1) (2017-12-04) ### Bug Fixes * **connectors:** check facet is refined before removing it. hierarchicalMenu / menu ([67ae035](https://github.com/algolia/instantsearch.js/commit/67ae035)) * **poweredBy:** minify slightly and make into correct URL ([#2615](https://github.com/algolia/instantsearch.js/issues/2615)) ([2b7d747](https://github.com/algolia/instantsearch.js/commit/2b7d747)), closes [#2613](https://github.com/algolia/instantsearch.js/issues/2613) # [2.3.0](https://github.com/algolia/instantsearch.js/compare/v2.3.0-beta.7...v2.3.0) (2017-11-30) ### Bug Fixes * **InstantSearch.dispose:** dont call `getConfiguration` of URLSync widget ([#2604](https://github.com/algolia/instantsearch.js/issues/2604)) ([3234b12](https://github.com/algolia/instantsearch.js/commit/3234b12)) * **connectors:** prefer wrappers over bind ([#2575](https://github.com/algolia/instantsearch.js/issues/2575)) ([f8e0e00](https://github.com/algolia/instantsearch.js/commit/f8e0e00)) * **connectHierarchicalMenu:** do not return if facet not set ([#2521](https://github.com/algolia/instantsearch.js/issues/2521)) ([26e99fb](https://github.com/algolia/instantsearch.js/commit/26e99fb)) ### Features * **core:** provide information about stalled search to widgets ([#2569](https://github.com/algolia/instantsearch.js/issues/2569)) ([d104be1](https://github.com/algolia/instantsearch.js/commit/d104be1)) * **core:** InstantSearch hot remove/add widgets ([#2384](https://github.com/algolia/instantsearch.js/issues/2384)) ([cfc1710](https://github.com/algolia/instantsearch.js/commit/cfc1710)) * **refinementList:** add escapeFacetHits parameter ([#2507](https://github.com/algolia/instantsearch.js/issues/2507)) ([9b1b7ee](https://github.com/algolia/instantsearch.js/commit/9b1b7ee)) * **breadcrumb:** Add the breadcrumb widget ([#2451](https://github.com/algolia/instantsearch.js/issues/2451)) ([11d78f0](https://github.com/algolia/instantsearch.js/commit/11d78f0)), closes [#2299](https://github.com/algolia/instantsearch.js/issues/2299) * **connectRange:** round the range based on precision ([#2498](https://github.com/algolia/instantsearch.js/issues/2498)) ([d4df45d](https://github.com/algolia/instantsearch.js/commit/d4df45d)) * **rangeInput:** add rangeInput widget ([#2440](https://github.com/algolia/instantsearch.js/issues/2440)) ([7916d16](https://github.com/algolia/instantsearch.js/commit/7916d16)) ## [2.2.5](https://github.com/algolia/instantsearch.js/compare/v2.2.4...v2.2.5) (2017-11-20) ### Bug Fixes * **searchbox:** fix usage of custom reset template ([#2585](https://github.com/algolia/instantsearch.js/issues/2585)) ([aad92b9](https://github.com/algolia/instantsearch.js/commit/aad92b9)), closes [#2528](https://github.com/algolia/instantsearch.js/issues/2528) ## [2.2.4](https://github.com/algolia/instantsearch.js/compare/v2.2.3...v2.2.4) (2017-11-13) ### Bug Fixes * **numericSelector:** make default value possible ([#2565](https://github.com/algolia/instantsearch.js/issues/2565)) ([5664f98](https://github.com/algolia/instantsearch.js/commit/5664f98)) ## [2.2.3](https://github.com/algolia/instantsearch.js/compare/v2.2.2...v2.2.3) (2017-11-07) ### Bug Fixes * **connectRefinementList:** add label to searched items ([#2553](https://github.com/algolia/instantsearch.js/issues/2553)) ([ec810fa](https://github.com/algolia/instantsearch.js/commit/ec810fa)) * **refinementList:** fix facet exhaustivity check ([#2554](https://github.com/algolia/instantsearch.js/issues/2554)) ([0f1bf08](https://github.com/algolia/instantsearch.js/commit/0f1bf08)), closes [#2552](https://github.com/algolia/instantsearch.js/issues/2552) * **theme:** searchbar should have normal size input ([#2545](https://github.com/algolia/instantsearch.js/issues/2545)) ([50d99f0](https://github.com/algolia/instantsearch.js/commit/50d99f0)) ## [2.2.2](https://github.com/algolia/instantsearch.js/compare/v2.2.1...v2.2.2) (2017-10-30) ### Bug Fixes * **connectRefinementList:** set default value for limit ([#2517](https://github.com/algolia/instantsearch.js/issues/2517)) ([32918c9](https://github.com/algolia/instantsearch.js/commit/32918c9)) * **MenuSelect:** switch from react to preact-compat ([#2513](https://github.com/algolia/instantsearch.js/issues/2513)) ([06aa626](https://github.com/algolia/instantsearch.js/commit/06aa626)) * **range-slider:** add option `collapsible` ([#2502](https://github.com/algolia/instantsearch.js/issues/2502)) ([e78399d](https://github.com/algolia/instantsearch.js/commit/e78399d)), closes [#2501](https://github.com/algolia/instantsearch.js/issues/2501) * **url-sync:** make URLSync consistent even if search is tampered ([392927e](https://github.com/algolia/instantsearch.js/commit/392927e)), closes [#2523](https://github.com/algolia/instantsearch.js/issues/2523) ## [2.2.1](https://github.com/algolia/instantsearch.js/compare/v2.2.0...v2.2.1) (2017-10-16) ### Bug Fixes * **connectRangeSlider:** only clear the refinement on the current attribute ([#2459](https://github.com/algolia/instantsearch.js/issues/2459)) ([7cebf58](https://github.com/algolia/instantsearch.js/commit/7cebf58)) * **menuSelect:** select in userCssClasses ([#2455](https://github.com/algolia/instantsearch.js/issues/2455)) ([0eb3dc8](https://github.com/algolia/instantsearch.js/commit/0eb3dc8)) * **menuSelect:** use preact instead of React ([#2460](https://github.com/algolia/instantsearch.js/issues/2460)) ([35ccae8](https://github.com/algolia/instantsearch.js/commit/35ccae8)) * **test:** correctly reset the wired dependency ([#2461](https://github.com/algolia/instantsearch.js/issues/2461)) ([1f7f4ed](https://github.com/algolia/instantsearch.js/commit/1f7f4ed)) # [2.2.0](https://github.com/algolia/instantsearch.js/compare/v2.1.6...v2.2.0) (2017-10-03) ### Bug Fixes * **build:** minify css with `csso` instead of unminify css ([#2419](https://github.com/algolia/instantsearch.js/issues/2419)) ([12f96b8](https://github.com/algolia/instantsearch.js/commit/12f96b8)), closes [#2375](https://github.com/algolia/instantsearch.js/issues/2375) * **clear-all:** display the query when clearsQuery is true ([#2414](https://github.com/algolia/instantsearch.js/issues/2414)) ([6921895](https://github.com/algolia/instantsearch.js/commit/6921895)) * **range-slider:** Fix slider boundaries ([#2408](https://github.com/algolia/instantsearch.js/issues/2408)) ([bea43db](https://github.com/algolia/instantsearch.js/commit/bea43db)), closes [#2386](https://github.com/algolia/instantsearch.js/issues/2386) * **selector:** root classname is applied twice ([#2423](https://github.com/algolia/instantsearch.js/issues/2423)) ([44dca11](https://github.com/algolia/instantsearch.js/commit/44dca11)), closes [#2396](https://github.com/algolia/instantsearch.js/issues/2396) [#2397](https://github.com/algolia/instantsearch.js/issues/2397) * **webpack.dev:** sourcemaps in dev ([#2422](https://github.com/algolia/instantsearch.js/issues/2422)) ([ba6ca0a](https://github.com/algolia/instantsearch.js/commit/ba6ca0a)) ### Features * **menu-select:** add menu select widget ([#2316](https://github.com/algolia/instantsearch.js/issues/2316)) ([680f9bd](https://github.com/algolia/instantsearch.js/commit/680f9bd)) # [2.2.0-beta.1](https://github.com/algolia/instantsearch.js/compare/v2.1.4...v2.2.0-beta.1) (2017-09-18) ### Features * **analytics:** Push pagination ([#2337](https://github.com/algolia/instantsearch.js/issues/2337)) ([94ce086](https://github.com/algolia/instantsearch.js/commit/94ce086)) * **hitsPerPageSelector:** default hits per page setting ([4efd43e](https://github.com/algolia/instantsearch.js/commit/4efd43e)) * **hitsPerPageSelector:** default hits per page setting ([355f080](https://github.com/algolia/instantsearch.js/commit/355f080)) ## [2.1.6](https://github.com/algolia/instantsearch.js/compare/v2.1.5...v2.1.6) (2017-09-26) ### Bug Fixes * **deps:** update dependency documentation to v^5.0.0 ([#2355](https://github.com/algolia/instantsearch.js/issues/2355)) ([489647a](https://github.com/algolia/instantsearch.js/commit/489647a)) * **searchbox:** use initial input value if provided in the dom ([#2342](https://github.com/algolia/instantsearch.js/issues/2342)) ([180902a](https://github.com/algolia/instantsearch.js/commit/180902a)), closes [#2289](https://github.com/algolia/instantsearch.js/issues/2289) ## [2.1.5](https://github.com/algolia/instantsearch.js/compare/v2.1.4...v2.1.5) (2017-09-25) ### Bug Fixes * **deps:** update dependency algolia-frontend-components to v^0.0.33 ([#2341](https://github.com/algolia/instantsearch.js/issues/2341)) ([16994d8](https://github.com/algolia/instantsearch.js/commit/16994d8)) * **price-ranges:** update call to refine ([#2377](https://github.com/algolia/instantsearch.js/issues/2377)) ([34915d7](https://github.com/algolia/instantsearch.js/commit/34915d7)) * **slider:** Fix range slider pips and value 0 ([#2350](https://github.com/algolia/instantsearch.js/issues/2350)) ([fa0dc09](https://github.com/algolia/instantsearch.js/commit/fa0dc09)), closes [#2343](https://github.com/algolia/instantsearch.js/issues/2343) ## [2.1.4](https://github.com/algolia/instantsearch.js/compare/v2.1.3...v2.1.4) (2017-09-14) ### Bug Fixes * **release-script:** Add the generation of changelog for the release ([#2333](https://github.com/algolia/instantsearch.js/issues/2333)) ([9a2f70b](https://github.com/algolia/instantsearch.js/commit/9a2f70b)) * **slider:** edge case when min > max ([#2336](https://github.com/algolia/instantsearch.js/issues/2336)) ([8830ab0](https://github.com/algolia/instantsearch.js/commit/8830ab0)) * **slider:** Fix range slider dev env ([#2320](https://github.com/algolia/instantsearch.js/issues/2320)) ([e78de70](https://github.com/algolia/instantsearch.js/commit/e78de70)) * **slider:** use algolia fork of rheostat ([#2335](https://github.com/algolia/instantsearch.js/issues/2335)) ([9eae009](https://github.com/algolia/instantsearch.js/commit/9eae009)) ## [2.1.3](https://github.com/algolia/instantsearch.js/compare/v2.1.2...v2.1.3) (2017-09-05) ### Bug Fixes * **Pagination:** add `autohideContainerHOC` to ([#2296](https://github.com/algolia/instantsearch.js/issues/2296)) ([545f076](https://github.com/algolia/instantsearch.js/commit/545f076)) * **sffv:** no error when not providing noResults and no results ([#2310](https://github.com/algolia/instantsearch.js/issues/2310)) ([cc02b71](https://github.com/algolia/instantsearch.js/commit/cc02b71)), closes [#2087](https://github.com/algolia/instantsearch.js/issues/2087) ## [2.1.2](https://github.com/algolia/instantsearch.js/compare/v2.1.1...v2.1.2) (2017-08-24) ### Bug Fixes * **es:** wrong path to files ([#2295](https://github.com/algolia/instantsearch.js/issues/2295)) ([a437e19](https://github.com/algolia/instantsearch.js/commit/a437e19)) ## [2.1.1](https://github.com/algolia/instantsearch.js/compare/v2.1.0...v2.1.1) (2017-08-23) ### Bug Fixes * **build:** provide unminified css as well ([#2292](https://github.com/algolia/instantsearch.js/issues/2292)) ([a79e067](https://github.com/algolia/instantsearch.js/commit/a79e067)) # [2.1.0](https://github.com/algolia/instantsearch.js/compare/v2.1.0-beta.4...v2.1.0) (2017-08-21) ### Bug Fixes * **nvmrc:** upgrade nodejs version ([#2291](https://github.com/algolia/instantsearch.js/issues/2291)) ([94529d4](https://github.com/algolia/instantsearch.js/commit/94529d4)) ## [2.0.2](https://github.com/algolia/instantsearch.js/compare/v2.0.1...v2.0.2) (2017-07-24) ### Bug Fixes * **doc:** Cosmetic change ([48bb128](https://github.com/algolia/instantsearch.js/commit/48bb128)) * **search-box:** fix magnifier and reset customization ([4adfade](https://github.com/algolia/instantsearch.js/commit/4adfade)) * **theme:** enforce box-sizing: border-box ([e26e50d](https://github.com/algolia/instantsearch.js/commit/e26e50d)) * **url-sync:** remove is_v from url ([f19a1d5](https://github.com/algolia/instantsearch.js/commit/f19a1d5)), closes [#2233](https://github.com/algolia/instantsearch.js/issues/2233) ## [2.0.1](https://github.com/algolia/instantsearch.js/compare/v2.0.0...v2.0.1) (2017-07-12) # [2.0.0](https://github.com/algolia/instantsearch.js/compare/v1.11.15...v2.0.0) (2017-07-01) ### Bug Fixes * **argos-ci:** blur the active element ([66d0551](https://github.com/algolia/instantsearch.js/commit/66d0551)) * **connectNumericRefinementList:** reset page on refine ([22ec08d](https://github.com/algolia/instantsearch.js/commit/22ec08d)) * **doc.build:** watch & rebuild `.pug` ([16d8542](https://github.com/algolia/instantsearch.js/commit/16d8542)) * **doc.build/autoprefixer:** update mtime for onlyChanged plugin ([3b83e58](https://github.com/algolia/instantsearch.js/commit/3b83e58)) * **escapeHits:** dont apply configuration if not requested ([c89f99d](https://github.com/algolia/instantsearch.js/commit/c89f99d)) ### Features * **searchFunction:** make search function provide a better API ([8fc0831](https://github.com/algolia/instantsearch.js/commit/8fc0831)) # [2.0.0-beta.5](https://github.com/algolia/instantsearch.js/compare/v1.11.12...v2.0.0-beta.5) (2017-06-01) ### Bug Fixes * **Slider:** dont call `refine()` when it's disabled ([f1eabc9](https://github.com/algolia/instantsearch.js/commit/f1eabc9)) ### Features * **hits:** opt-in xss filtering for hits and infinite hits. FIX #2138 ([4f67b48](https://github.com/algolia/instantsearch.js/commit/4f67b48)), closes [#2138](https://github.com/algolia/instantsearch.js/issues/2138) # [2.0.0-beta.4](https://github.com/algolia/instantsearch.js/compare/v1.11.11...v2.0.0-beta.4) (2017-05-24) ### Bug Fixes * **misc:** IE 11 support ([072edfe](https://github.com/algolia/instantsearch.js/commit/072edfe)) * **misc:** IE11 support without using transpiler ([324f062](https://github.com/algolia/instantsearch.js/commit/324f062)) * **show-more:** should hide button when show more is not available (#2161) ([fbca3e6](https://github.com/algolia/instantsearch.js/commit/fbca3e6)), closes [#2160](https://github.com/algolia/instantsearch.js/issues/2160) * **Slider:** handle edge case where `min === max` ([22a5614](https://github.com/algolia/instantsearch.js/commit/22a5614)) * **Slider:** restore `slider--handle-lower` && `slider--handle-upper` ([64d7ad2](https://github.com/algolia/instantsearch.js/commit/64d7ad2)) # [2.0.0-beta.2](https://github.com/algolia/instantsearch.js/compare/v1.11.9...v2.0.0-beta.2) (2017-05-17) ### Bug Fixes * **autoHideContainer:** dont prevent render with `shouldComponentUpdate` ([8c4b13f](https://github.com/algolia/instantsearch.js/commit/8c4b13f)) * **clearsQuery:** not applied when only the query was not empty ([e7976ad](https://github.com/algolia/instantsearch.js/commit/e7976ad)) * **connectors:** ensure `widgetParams` is at least an `{}` ([0c0e98f](https://github.com/algolia/instantsearch.js/commit/0c0e98f)) * **connectRefinementList:** currentRefinements: return an array instead of first item ([a53223a](https://github.com/algolia/instantsearch.js/commit/a53223a)), closes [#2102](https://github.com/algolia/instantsearch.js/issues/2102) * **dev:docs:** dont watch `/docgen/rootFiles` ([ab1a7f5](https://github.com/algolia/instantsearch.js/commit/ab1a7f5)) * **doc:** add doc for isFirstRendering ([cea6739](https://github.com/algolia/instantsearch.js/commit/cea6739)) * **docs:** dont filter out `p.type.type` ([881659a](https://github.com/algolia/instantsearch.js/commit/881659a)) * **documentation.js:** Support for record types ([219ecd9](https://github.com/algolia/instantsearch.js/commit/219ecd9)) * **documentationjs:** add support litteral string types in type format ([2a08e7d](https://github.com/algolia/instantsearch.js/commit/2a08e7d)) * **documentationjs:** deeper related types ([6e3121e](https://github.com/algolia/instantsearch.js/commit/6e3121e)) * **documentationjs:** find related type in TypeApplication ([e0487ee](https://github.com/algolia/instantsearch.js/commit/e0487ee)) * **documentationjs:** fix 2+ depth structs ([4c8b7ec](https://github.com/algolia/instantsearch.js/commit/4c8b7ec)) * **documentationjs:** fixed default value parameter ([b62cbc7](https://github.com/algolia/instantsearch.js/commit/b62cbc7)) * **documentationjs:** records display with , ([8a968f2](https://github.com/algolia/instantsearch.js/commit/8a968f2)) * **documentationjs:** Updgrade to RC + fixes ([e9f0361](https://github.com/algolia/instantsearch.js/commit/e9f0361)) * **infinite-hits:** Remove hitsPerPage option (#2128) ([c13e377](https://github.com/algolia/instantsearch.js/commit/c13e377)) * **live-example:** adapt regex for matching connectors ([774254c](https://github.com/algolia/instantsearch.js/commit/774254c)) * **pagination:** fix zealous find/replace ([e269d87](https://github.com/algolia/instantsearch.js/commit/e269d87)) * **price-ranges:** fix test ([fd65cb3](https://github.com/algolia/instantsearch.js/commit/fd65cb3)) * **price-ranges:** New API uses ranges ([a5a6916](https://github.com/algolia/instantsearch.js/commit/a5a6916)) * **refinementList:** reimplement show more on refinement list ([72655ab](https://github.com/algolia/instantsearch.js/commit/72655ab)) * **refinementList:** sffv fix thanks [@julienpa](https://github.com/julienpa) ([30e0e9a](https://github.com/algolia/instantsearch.js/commit/30e0e9a)) * **sffv:** Fix exhaustive facets ([0cadcc3](https://github.com/algolia/instantsearch.js/commit/0cadcc3)) * **sortby:** Consistent across widget / connectors + migration ([8e366cc](https://github.com/algolia/instantsearch.js/commit/8e366cc)) * **widgets/price-ranges:** wrong compute of `templateProps` ([be5e063](https://github.com/algolia/instantsearch.js/commit/be5e063)) ### Features * **connectHierarchicalMenu:** remove `currentRefinement` ([3912aaf](https://github.com/algolia/instantsearch.js/commit/3912aaf)) * **connectHits:** typo `widgetOptions` -> `widgetParams` ([4420231](https://github.com/algolia/instantsearch.js/commit/4420231)) * **connector:** Add hierarchical menu connector ([f727949](https://github.com/algolia/instantsearch.js/commit/f727949)) * **connector:** add infinite hits connector ([cdf8675](https://github.com/algolia/instantsearch.js/commit/cdf8675)) * **connector:** add instantsearchInstance to pagination render ([4fa96dc](https://github.com/algolia/instantsearch.js/commit/4fa96dc)) * **connector:** add missing jsDoc descriptions ([e26e8e2](https://github.com/algolia/instantsearch.js/commit/e26e8e2)) * **connector:** add range-slider ([1a02798](https://github.com/algolia/instantsearch.js/commit/1a02798)) * **connector:** add tests for connectClearAll and connectHierarchicalMenu ([0eb29ec](https://github.com/algolia/instantsearch.js/commit/0eb29ec)) * **connector:** Adds hits and menu connectors ([77083b7](https://github.com/algolia/instantsearch.js/commit/77083b7)) * **connector:** Clear and CurrentRefinedValues ([02f7d3e](https://github.com/algolia/instantsearch.js/commit/02f7d3e)) * **connector:** clearAll connector (iteration 2) ([90aa02e](https://github.com/algolia/instantsearch.js/commit/90aa02e)) * **connector:** clearAll jsDoc + eslint fixes ([430a420](https://github.com/algolia/instantsearch.js/commit/430a420)) * **connector:** complete jsdoc + pass instantsearch to view ([e125931](https://github.com/algolia/instantsearch.js/commit/e125931)) * **connector:** connectClearAll documentation ([9b153aa](https://github.com/algolia/instantsearch.js/commit/9b153aa)) * **connector:** connectClearAll iteration 2 (fix) ([03653f1](https://github.com/algolia/instantsearch.js/commit/03653f1)) * **connector:** connectClearAll test ([5409157](https://github.com/algolia/instantsearch.js/commit/5409157)) * **connector:** connectCurrentRefinedValues (iteration 2) ([68408de](https://github.com/algolia/instantsearch.js/commit/68408de)) * **connector:** connectHierarchicalMenu (iteration 2) ([589454c](https://github.com/algolia/instantsearch.js/commit/589454c)) * **connector:** connectHierarchicalMenu jsDoc ([e166090](https://github.com/algolia/instantsearch.js/commit/e166090)) * **connector:** connectHits (iteration 2) ([bca09af](https://github.com/algolia/instantsearch.js/commit/bca09af)) * **connector:** connectHitsPerPageSelector (iteration 2) ([26bb273](https://github.com/algolia/instantsearch.js/commit/26bb273)) * **connector:** connectInfiniteHits (iteration 2) ([410459c](https://github.com/algolia/instantsearch.js/commit/410459c)) * **connector:** connectNumericRefinementList (iteration 2) ([bfcf860](https://github.com/algolia/instantsearch.js/commit/bfcf860)) * **connector:** connectNumericSelector (iteration 2) ([1eda8a2](https://github.com/algolia/instantsearch.js/commit/1eda8a2)) * **connector:** connectNumericSelector jsDoc ([760fcea](https://github.com/algolia/instantsearch.js/commit/760fcea)) * **connector:** connectRefinementList jsdoc + start document bool isFirstRendering ([52d13de](https://github.com/algolia/instantsearch.js/commit/52d13de)) * **connector:** connectStats second iteration ([82b1cb3](https://github.com/algolia/instantsearch.js/commit/82b1cb3)) * **connector:** connectToggle second iteration ([73b0878](https://github.com/algolia/instantsearch.js/commit/73b0878)) * **connector:** fix createURL usage to generate correct urls ([fdf59d7](https://github.com/algolia/instantsearch.js/commit/fdf59d7)) * **connector:** fix no param usage on custom infiniteHits ([961348a](https://github.com/algolia/instantsearch.js/commit/961348a)) * **connector:** fix parameter consistency in connectClearAll ([9ddffd8](https://github.com/algolia/instantsearch.js/commit/9ddffd8)) * **connector:** Fix parameters for toggle connector ([f96671c](https://github.com/algolia/instantsearch.js/commit/f96671c)) * **connector:** hits-per-page-selector connector refactoring ([dd794e0](https://github.com/algolia/instantsearch.js/commit/dd794e0)) * **connector:** jsDoc + check rendering function ([86f9739](https://github.com/algolia/instantsearch.js/commit/86f9739)) * **connector:** jsDoc connectPagination ([3b284de](https://github.com/algolia/instantsearch.js/commit/3b284de)) * **connector:** jsDoc for connectMenu ([626d5f1](https://github.com/algolia/instantsearch.js/commit/626d5f1)) * **connector:** jsDoc updates ([c924043](https://github.com/algolia/instantsearch.js/commit/c924043)) * **connector:** move clearAll as a rendering option ([ce41cde](https://github.com/algolia/instantsearch.js/commit/ce41cde)) * **connector:** Numeric selector ([0dc42d2](https://github.com/algolia/instantsearch.js/commit/0dc42d2)) * **connector:** numericRefinementList connector ([918d971](https://github.com/algolia/instantsearch.js/commit/918d971)) * **connector:** pagination connector ([7a876f3](https://github.com/algolia/instantsearch.js/commit/7a876f3)) * **connector:** price ranges connector ([d8bed96](https://github.com/algolia/instantsearch.js/commit/d8bed96)) * **connector:** provide consistent interface for searchbox renderer ([17d8301](https://github.com/algolia/instantsearch.js/commit/17d8301)) * **connector:** provide instantsearch instance at render ([12a7935](https://github.com/algolia/instantsearch.js/commit/12a7935)) * **connector:** refactor search function ([618dca2](https://github.com/algolia/instantsearch.js/commit/618dca2)) * **connector:** refinement list connector ([c8fcf4e](https://github.com/algolia/instantsearch.js/commit/c8fcf4e)) * **connector:** remove legacy implementation of toggle ([04437b0](https://github.com/algolia/instantsearch.js/commit/04437b0)) * **connector:** remove non relevant instantsearch API from test ([c5dce5c](https://github.com/algolia/instantsearch.js/commit/c5dce5c)) * **connector:** remove unused parameter to searchbox connector ([e639f65](https://github.com/algolia/instantsearch.js/commit/e639f65)) * **connector:** searchbox connector ([70f8e1f](https://github.com/algolia/instantsearch.js/commit/70f8e1f)) * **connector:** small internal refactoring for SFFV ([cb5c1fa](https://github.com/algolia/instantsearch.js/commit/cb5c1fa)) * **connector:** sort by selector connector ([b9847cf](https://github.com/algolia/instantsearch.js/commit/b9847cf)) * **connector:** star rating connector ([9996b4d](https://github.com/algolia/instantsearch.js/commit/9996b4d)) * **connector:** stats connector ([680743b](https://github.com/algolia/instantsearch.js/commit/680743b)) * **connector:** test connectHits ([89c86a5](https://github.com/algolia/instantsearch.js/commit/89c86a5)) * **connector:** test connectHitsPerPageSelector ([9caab02](https://github.com/algolia/instantsearch.js/commit/9caab02)) * **connector:** test connectInfiniteHits ([e67e75e](https://github.com/algolia/instantsearch.js/commit/e67e75e)) * **connector:** test connectMenu ([03c6f11](https://github.com/algolia/instantsearch.js/commit/03c6f11)) * **connector:** test connectNumericRefinementList ([2f26251](https://github.com/algolia/instantsearch.js/commit/2f26251)) * **connector:** test connectNumericSelector ([182779b](https://github.com/algolia/instantsearch.js/commit/182779b)) * **connector:** test connectPagination ([6f125b7](https://github.com/algolia/instantsearch.js/commit/6f125b7)) * **connector:** test connectPriceRanges ([f5dfba7](https://github.com/algolia/instantsearch.js/commit/f5dfba7)) * **connector:** test connectRangeSlider ([4f6c180](https://github.com/algolia/instantsearch.js/commit/4f6c180)) * **connector:** test connectSearchBox ([b4d7e1b](https://github.com/algolia/instantsearch.js/commit/b4d7e1b)) * **connector:** test connectSortBySelector ([e8825df](https://github.com/algolia/instantsearch.js/commit/e8825df)) * **connector:** test connectStarRating ([0c16f15](https://github.com/algolia/instantsearch.js/commit/0c16f15)), closes [#2002](https://github.com/algolia/instantsearch.js/issues/2002) * **connector:** test connectStats ([c992288](https://github.com/algolia/instantsearch.js/commit/c992288)) * **connector:** test connectToggle ([441293d](https://github.com/algolia/instantsearch.js/commit/441293d)) * **connector:** toggle connector ([bf9a9c0](https://github.com/algolia/instantsearch.js/commit/bf9a9c0)) * **connector:** update doc, move setValue to refine in SortBySelector ([2486f36](https://github.com/algolia/instantsearch.js/commit/2486f36)) * **connector:** update jsDoc descriptions ([f83022a](https://github.com/algolia/instantsearch.js/commit/f83022a)) * **connectors:** `refinement-list` widget (iteration2) ([1c6c3a5](https://github.com/algolia/instantsearch.js/commit/1c6c3a5)) * **connectors:** `setValue()` -> `refine()` / `currentValue` -> `currentRefinement` ([ec7806c](https://github.com/algolia/instantsearch.js/commit/ec7806c)) * **connectors:** `sortBy` to `['isRefined', 'count:desc']` ([01219f1](https://github.com/algolia/instantsearch.js/commit/01219f1)) * **connectors:** add `currentRefinement` on `hierarchical-menu` ([154cdb5](https://github.com/algolia/instantsearch.js/commit/154cdb5)) * **connectors:** connectPagination (iteration2) ([8a615f6](https://github.com/algolia/instantsearch.js/commit/8a615f6)) * **connectors:** connectPriceRanges (iteration2) ([e34968e](https://github.com/algolia/instantsearch.js/commit/e34968e)) * **connectors:** connectRangeSlider (iteration2) ([6073d94](https://github.com/algolia/instantsearch.js/commit/6073d94)) * **connectors:** connectSearchBox (iteration2) ([3161c9b](https://github.com/algolia/instantsearch.js/commit/3161c9b)) * **connectors:** connectSortBySelector (iteration 2) ([dec2d31](https://github.com/algolia/instantsearch.js/commit/dec2d31)) * **connectors:** connectStarRating (iteration2) ([7ef7b6b](https://github.com/algolia/instantsearch.js/commit/7ef7b6b)) * **connectors:** connectToggle, forward initial options to render ([704a455](https://github.com/algolia/instantsearch.js/commit/704a455)) * **connectors:** dissociate logic & view for `menu` widget ([5a02c88](https://github.com/algolia/instantsearch.js/commit/5a02c88)) * **connectors:** expose connectors on `instantsearch` instance ([ff799d0](https://github.com/algolia/instantsearch.js/commit/ff799d0)) * **connectors:** forward `widgetParams` to `renderFn` ([54222a3](https://github.com/algolia/instantsearch.js/commit/54222a3)) * **connectors:** jsDoc connectHitsPerPageSelector ([75243b0](https://github.com/algolia/instantsearch.js/commit/75243b0)) * **connectors:** provide `currentRefinement` on menu ([fb7bc5e](https://github.com/algolia/instantsearch.js/commit/fb7bc5e)) * **connectors:** provide `currentRefinement` on numeric refinement list ([91f7928](https://github.com/algolia/instantsearch.js/commit/91f7928)) * **connectors.numeric-selector:** `currentValue` -> `currentRefinement` / `setValue()` -> `refine()` ([998faf1](https://github.com/algolia/instantsearch.js/commit/998faf1)) * **connectors.price-ranges:** provides `currentRefiment` value ([39af437](https://github.com/algolia/instantsearch.js/commit/39af437)) * **connectors.refinement-list:** provide `currentRefinement` to `renderFn` ([7e86be3](https://github.com/algolia/instantsearch.js/commit/7e86be3)) * **connectors.star-rating:** provide `currentRefinement` value ([c08b3e4](https://github.com/algolia/instantsearch.js/commit/c08b3e4)) * **connectRefinementList:** first good iteration ([88fd6d5](https://github.com/algolia/instantsearch.js/commit/88fd6d5)) * **doc:** re-bootstrap doc based on instantsearch-android ([e4e816e](https://github.com/algolia/instantsearch.js/commit/e4e816e)) * **docs:** bootstrap v2 docs ([0db6caf](https://github.com/algolia/instantsearch.js/commit/0db6caf)) * **docs:** pages structure ([fe89dcf](https://github.com/algolia/instantsearch.js/commit/fe89dcf)) * **getting-started:** add `.zip` boilerplate ([7d3769c](https://github.com/algolia/instantsearch.js/commit/7d3769c)) * **getting-started:** add result example of guide ([78d9017](https://github.com/algolia/instantsearch.js/commit/78d9017)) * **live-example:** add support of connectors ([e4f3158](https://github.com/algolia/instantsearch.js/commit/e4f3158)) * **live-example:** include jquery on connectors example pages ([f32936f](https://github.com/algolia/instantsearch.js/commit/f32936f)) * **main:** export all the widgets at once ([4bc2d21](https://github.com/algolia/instantsearch.js/commit/4bc2d21)) * **numeric-refinement-list:** `facetValues` -> `items` / `toggleRefinement` -> `refine` ([eb2c993](https://github.com/algolia/instantsearch.js/commit/eb2c993)) * **pagination:** `setPage()` -> `refine()` / `currentPage` -> `currentRefinement` ([f783fea](https://github.com/algolia/instantsearch.js/commit/f783fea)) * **range-slider:** use `rheostat` as slider component (#2142) ([910a0a0](https://github.com/algolia/instantsearch.js/commit/910a0a0)) * **searchFunction:** Update API, fix #1924 ([c7beb1d](https://github.com/algolia/instantsearch.js/commit/c7beb1d)), closes [#1924](https://github.com/algolia/instantsearch.js/issues/1924) * **sort-by-selector:** `currentValue` -> `currentRefinement` ([e94c8c7](https://github.com/algolia/instantsearch.js/commit/e94c8c7)) * **Template:** remove support for react element ([ca2ab44](https://github.com/algolia/instantsearch.js/commit/ca2ab44)) ## [1.11.15](https://github.com/algolia/instantsearch.js/compare/v1.11.14...v1.11.15) (2017-06-20) ### Bug Fixes * **numeric-refinement-list:** reset page on refine ([ee55ccb](https://github.com/algolia/instantsearch.js/commit/ee55ccb)) ## [1.11.14](https://github.com/algolia/instantsearch.js/compare/v1.11.13...v1.11.14) (2017-06-19) ### Bug Fixes * **powered-by:** update logo ([7e68b51](https://github.com/algolia/instantsearch.js/commit/7e68b51)), closes [#2126](https://github.com/algolia/instantsearch.js/issues/2126) ## [1.11.13](https://github.com/algolia/instantsearch.js/compare/v1.11.12...v1.11.13) (2017-06-07) ### Bug Fixes * **url-sync:** reverting back to using `change` event (#2183) ([07f4be0](https://github.com/algolia/instantsearch.js/commit/07f4be0)), closes [#2173](https://github.com/algolia/instantsearch.js/issues/2173) [#2171](https://github.com/algolia/instantsearch.js/issues/2171) ## [1.11.12](https://github.com/algolia/instantsearch.js/compare/v1.11.11...v1.11.12) (2017-05-30) ### Bug Fixes * **sffv:** when using a large limit, retain the search (#2163) ([3d95d4c](https://github.com/algolia/instantsearch.js/commit/3d95d4c)), closes [#2156](https://github.com/algolia/instantsearch.js/issues/2156) ## [1.11.10](https://github.com/algolia/instantsearch.js/compare/v1.11.9...v1.11.10) (2017-05-17) ## [1.11.9](https://github.com/algolia/instantsearch.js/compare/v1.11.8...v1.11.9) (2017-05-17) ## [1.11.8](https://github.com/algolia/instantsearch.js/compare/v1.11.7...v1.11.8) (2017-05-16) ### Bug Fixes * **url-sync:** set firstRender to be class attribute ([22dbaeb](https://github.com/algolia/instantsearch.js/commit/22dbaeb)) ## [1.11.7](https://github.com/algolia/instantsearch.js/compare/v1.11.6...v1.11.7) (2017-04-24) ### Bug Fixes * **sffv:** add class for disabled state at the form level (#2122) ([029fa5f](https://github.com/algolia/instantsearch.js/commit/029fa5f)) * **sffv:** fixes typo (: was left) ([26d2845](https://github.com/algolia/instantsearch.js/commit/26d2845)) ## [1.11.6](https://github.com/algolia/instantsearch.js/compare/v1.11.5...v1.11.6) (2017-04-20) ### Bug Fixes * **CONTRIBUTING:** remove section about beta releases (#2109) ([5640131](https://github.com/algolia/instantsearch.js/commit/5640131)) * **sffv:** disable sffv input when few facet values FIX #2111 ([1e33c10](https://github.com/algolia/instantsearch.js/commit/1e33c10)), closes [#2111](https://github.com/algolia/instantsearch.js/issues/2111) ## [1.11.5](https://github.com/algolia/instantsearch.js/compare/v1.11.4...v1.11.5) (2017-04-12) ### Bug Fixes * **url-sync:** sync url on search (#2108) ([7f33ffb](https://github.com/algolia/instantsearch.js/commit/7f33ffb)) ## [1.11.4](https://github.com/algolia/instantsearch.js/compare/v1.11.3...v1.11.4) (2017-03-29) ### Bug Fixes * **autoHideContainer:** dont prevent render with `shouldComponentUpdate` (#2076) ([b520400](https://github.com/algolia/instantsearch.js/commit/b520400)) * **star-rating:** make max value inclusive ([f5fc41c](https://github.com/algolia/instantsearch.js/commit/f5fc41c)), closes [#2002](https://github.com/algolia/instantsearch.js/issues/2002) ## [1.11.3](https://github.com/algolia/instantsearch.js/compare/v1.11.2...v1.11.3) (2017-03-22) ### Bug Fixes * **Slider:** display disabled slider when `min === max` (#2041) ([511fdfd](https://github.com/algolia/instantsearch.js/commit/511fdfd)), closes [#2037](https://github.com/algolia/instantsearch.js/issues/2037) ## [1.11.2](https://github.com/algolia/instantsearch.js/compare/v1.11.1...v1.11.2) (2017-02-28) ### Bug Fixes * **searchBox:** avoid unwanted cursor jumps on hashchange (#2013) ([d0103db](https://github.com/algolia/instantsearch.js/commit/d0103db)), closes [#2012](https://github.com/algolia/instantsearch.js/issues/2012) ## [1.11.1](https://github.com/algolia/instantsearch.js/compare/v1.11.0...v1.11.1) (2017-02-14) ### Bug Fixes * **infinite-hits:** disable load more button when no more pages (#1973) ([745ed89](https://github.com/algolia/instantsearch.js/commit/745ed89)), closes [#1971](https://github.com/algolia/instantsearch.js/issues/1971) # [1.11.0](https://github.com/algolia/instantsearch.js/compare/v1.10.5...v1.11.0) (2017-02-12) ### Features * **analytics-widget:** add a new parameter pushInitialSearch (#1963) ([d777997](https://github.com/algolia/instantsearch.js/commit/d777997)) * **custom client:** allows to provide a custom JS client instance (#1948) ([cce4f2e](https://github.com/algolia/instantsearch.js/commit/cce4f2e)) * **InfiniteHits:** add new widget ([2d77e4b](https://github.com/algolia/instantsearch.js/commit/2d77e4b)) ## [1.10.5](https://github.com/algolia/instantsearch.js/compare/v1.10.4...v1.10.5) (2017-02-06) ### Bug Fixes * **urlSync:** update url only after threshold (#1917) ([b0f0cf1](https://github.com/algolia/instantsearch.js/commit/b0f0cf1)), closes [#1856](https://github.com/algolia/instantsearch.js/issues/1856) ## [1.10.4](https://github.com/algolia/instantsearch.js/compare/v1.10.3...v1.10.4) (2017-01-25) ## [1.10.3](https://github.com/algolia/instantsearch.js/compare/v1.10.2...v1.10.3) (2016-12-26) ### Bug Fixes * **sffv-searchbox:** update classnames to avoid conflicts (#1781) ([f53e8fd](https://github.com/algolia/instantsearch.js/commit/f53e8fd)) ## [1.10.2](https://github.com/algolia/instantsearch.js/compare/v1.10.1...v1.10.2) (2016-12-23) ### Bug Fixes * **url:** clear timeout on pop ([41ad9af](https://github.com/algolia/instantsearch.js/commit/41ad9af)) ## [1.10.1](https://github.com/algolia/instantsearch.js/compare/v1.10.0...v1.10.1) (2016-12-23) ### Bug Fixes * **url:** default param ([7a18e1c](https://github.com/algolia/instantsearch.js/commit/7a18e1c)) ### Features * **url:** add a beta updateOnEveryKeystroke option (#1779) ([63f73fe](https://github.com/algolia/instantsearch.js/commit/63f73fe)) # [1.10.0](https://github.com/algolia/instantsearch.js/compare/v1.9.0...v1.10.0) (2016-12-22) ### Features * **widget:** Search for facet values - refinement list (#1753) ([b9e20f3](https://github.com/algolia/instantsearch.js/commit/b9e20f3)) # [1.9.0](https://github.com/algolia/instantsearch.js/compare/v1.8.16...v1.9.0) (2016-12-14) ### Bug Fixes * **currentRefinedValues:** unescape disjunctive facet refinement names (#1574) ([9ab65c4](https://github.com/algolia/instantsearch.js/commit/9ab65c4)), closes [#1569](https://github.com/algolia/instantsearch.js/issues/1569) * **transformData:** default data is an object when not provided (#1570) ([8eeeeba](https://github.com/algolia/instantsearch.js/commit/8eeeeba)), closes [#1538](https://github.com/algolia/instantsearch.js/issues/1538) ### Features * **analytics:** new analytics widget to easily plug search to any analytics service ([09d8fda](https://github.com/algolia/instantsearch.js/commit/09d8fda)) * **retry strategy:** new retry strategy ([afdcc3c](https://github.com/algolia/instantsearch.js/commit/afdcc3c)) ## [1.8.16](https://github.com/algolia/instantsearch.js/compare/v1.8.15...v1.8.16) (2016-11-16) ## [1.8.15](https://github.com/algolia/instantsearch.js/compare/v1.8.14...v1.8.15) (2016-11-16) ### Bug Fixes * **priceRanges:** avoid displaying solo ranges (#1544) ([ff396f0](https://github.com/algolia/instantsearch.js/commit/ff396f0)), closes [#1536](https://github.com/algolia/instantsearch.js/issues/1536) * **priceRanges:** use formatNumber in defaultTemplate (#1559) ([557a501](https://github.com/algolia/instantsearch.js/commit/557a501)), closes [#1230](https://github.com/algolia/instantsearch.js/issues/1230) * **toggle:** support negative numeric values for on/off (#1551) ([e4d88e0](https://github.com/algolia/instantsearch.js/commit/e4d88e0)), closes [#1537](https://github.com/algolia/instantsearch.js/issues/1537) * **transformData:** always call transformData (#1555) ([49bfeca](https://github.com/algolia/instantsearch.js/commit/49bfeca)), closes [#1538](https://github.com/algolia/instantsearch.js/issues/1538) ## [1.8.14](https://github.com/algolia/instantsearch.js/compare/v1.8.13...v1.8.14) (2016-11-03) ### Bug Fixes * **slider:** avoid multi touch issues (#1501) ([0b8a242](https://github.com/algolia/instantsearch.js/commit/0b8a242)), closes [#1186](https://github.com/algolia/instantsearch.js/issues/1186) ## [1.8.13](https://github.com/algolia/instantsearch.js/compare/v1.8.12...v1.8.13) (2016-10-21) ### Bug Fixes * **searchbox:** poweredBy Algolia logo weren't visible in firefox ([39701f8](https://github.com/algolia/instantsearch.js/commit/39701f8)) ## [1.8.12](https://github.com/algolia/instantsearch.js/compare/v1.8.11...v1.8.12) (2016-10-19) ### Bug Fixes * **numericRefinementList:** classes on radio buttons (#1358) (#1432) ([fec6495](https://github.com/algolia/instantsearch.js/commit/fec6495)) ## [1.8.11](https://github.com/algolia/instantsearch.js/compare/v1.8.10...v1.8.11) (2016-10-07) ### Bug Fixes * **merge:** merge only plain object from searchParameters ([aab1c87](https://github.com/algolia/instantsearch.js/commit/aab1c87)) ## [1.8.10](https://github.com/algolia/instantsearch.js/compare/v1.8.9...v1.8.10) (2016-10-07) ### Bug Fixes * **lodash:** set lodash back to 4.15.0, fixes build, unknown issue for now ([ba4247e](https://github.com/algolia/instantsearch.js/commit/ba4247e)) ## [1.8.9](https://github.com/algolia/instantsearch.js/compare/v1.8.8...v1.8.9) (2016-10-07) ### Bug Fixes * **react:** avoid duplicating React ([59010f6](https://github.com/algolia/instantsearch.js/commit/59010f6)), closes [#1386](https://github.com/algolia/instantsearch.js/issues/1386) ## [1.8.8](https://github.com/algolia/instantsearch.js/compare/v1.8.6...v1.8.8) (2016-09-14) ### Bug Fixes * **numericSelector:** do not change state on init (#1280) ([cf27db3](https://github.com/algolia/instantsearch.js/commit/cf27db3)), closes [#1253](https://github.com/algolia/instantsearch.js/issues/1253) * **Slider:** default precision to 2 (#1279) ([552b9ea](https://github.com/algolia/instantsearch.js/commit/552b9ea)) ## [1.8.6](https://github.com/algolia/instantsearch.js/compare/v1.8.5...v1.8.6) (2016-09-12) ## [1.8.5](https://github.com/algolia/instantsearch.js/compare/v1.8.4...v1.8.5) (2016-09-06) ### Bug Fixes * **deps:** upgrade all deps 2016-09-05 (#1261) ([408d597](https://github.com/algolia/instantsearch.js/commit/408d597)) * **rangeSlider:** round pips numbers when step is integer (#1255) ([b993033](https://github.com/algolia/instantsearch.js/commit/b993033)), closes [#1254](https://github.com/algolia/instantsearch.js/issues/1254) ## [1.8.4](https://github.com/algolia/instantsearch.js/compare/v1.8.3...v1.8.4) (2016-08-29) ### Bug Fixes * **bundle:** switch back to React by default, create a preact build (#1228) ([4845868](https://github.com/algolia/instantsearch.js/commit/4845868)) ## [1.8.3](https://github.com/algolia/instantsearch.js/compare/v1.8.2...v1.8.3) (2016-08-29) ### Bug Fixes * **numericSelector:** if no currentValue found, use the first option ([ef56dfa](https://github.com/algolia/instantsearch.js/commit/ef56dfa)) * **poweredBy:** fixed Algolia logo version (#1223) ([aab3fc3](https://github.com/algolia/instantsearch.js/commit/aab3fc3)), closes [#1223](https://github.com/algolia/instantsearch.js/issues/1223) [#1222](https://github.com/algolia/instantsearch.js/issues/1222) * **Selector:** render a controlled component ([e9f6ff7](https://github.com/algolia/instantsearch.js/commit/e9f6ff7)) ### Performance Improvements * **filesize:** use preact in production build (#1224) ([5bb38f2](https://github.com/algolia/instantsearch.js/commit/5bb38f2)), closes [#1030](https://github.com/algolia/instantsearch.js/issues/1030) ## [1.8.2](https://github.com/algolia/instantsearch.js/compare/v1.8.1...v1.8.2) (2016-08-25) ### Bug Fixes * **lodash:** use lodash v4, reduce build size ([216d1e0](https://github.com/algolia/instantsearch.js/commit/216d1e0)) ## [1.8.1](https://github.com/algolia/instantsearch.js/compare/v1.8.0...v1.8.1) (2016-08-24) ### Bug Fixes * **searchBox:** handle BFCache browsers (#1212) ([7deb9c3](https://github.com/algolia/instantsearch.js/commit/7deb9c3)) * **toggle:** make autoHide check facetValue.count (#1213) ([86872eb](https://github.com/algolia/instantsearch.js/commit/86872eb)) # [1.8.0](https://github.com/algolia/instantsearch.js/compare/v1.7.1...v1.8.0) (2016-08-18) ### Bug Fixes * **documentation:** Change instantsearch.widgets.stats typo data.processingTimMS to data.processingTimeMS ([034703e](https://github.com/algolia/instantsearch.js/commit/034703e)) * **documentation:** Change responsiveNavigation.js & header.html to fix #1090 ([bf3a808](https://github.com/algolia/instantsearch.js/commit/bf3a808)), closes [#1090](https://github.com/algolia/instantsearch.js/issues/1090) * **nouislider:** fix the slider for nouislider 8.5.1 ([af8f56b](https://github.com/algolia/instantsearch.js/commit/af8f56b)) ### Features * **clearAll:** Add optional excludeAttributes to list protected filters ([fe6d19c](https://github.com/algolia/instantsearch.js/commit/fe6d19c)) ## [1.7.1](https://github.com/algolia/instantsearch.js/compare/v1.7.0...v1.7.1) (2016-07-28) ### Bug Fixes * **toggle:** add backward compatibility for previous toggle implem (#1154) ([a1973a0](https://github.com/algolia/instantsearch.js/commit/a1973a0)) # [1.7.0](https://github.com/algolia/instantsearch.js/compare/v1.6.4...v1.7.0) (2016-07-26) ### Bug Fixes * **searchParameters:** avoid mutating provided objects (#1148) ([0ea3bef](https://github.com/algolia/instantsearch.js/commit/0ea3bef)), closes [#1130](https://github.com/algolia/instantsearch.js/issues/1130) ### Features * **toggle:** Provide a better default widget (#1146) ([d54107e](https://github.com/algolia/instantsearch.js/commit/d54107e)), closes [#1096](https://github.com/algolia/instantsearch.js/issues/1096) [#919](https://github.com/algolia/instantsearch.js/issues/919) ## [1.6.4](https://github.com/algolia/instantsearch.js/compare/v1.6.3...v1.6.4) (2016-07-12) ## [1.6.3](https://github.com/algolia/instantsearch.js/compare/v1.6.2...v1.6.3) (2016-07-11) ### Bug Fixes * **Hits:** always render hits ([2e7bf8a](https://github.com/algolia/instantsearch.js/commit/2e7bf8a)), closes [#1100](https://github.com/algolia/instantsearch.js/issues/1100) ## [1.6.2](https://github.com/algolia/instantsearch.js/compare/v1.6.1...v1.6.2) (2016-07-11) ### Bug Fixes * **paginationLink:** it's aria-label not ariaLabel (#1125) ([70a190c](https://github.com/algolia/instantsearch.js/commit/70a190c)) * **pricesRange:** fill the form according to the current refinement (#1126) ([12ebde7](https://github.com/algolia/instantsearch.js/commit/12ebde7)), closes [#1009](https://github.com/algolia/instantsearch.js/issues/1009) * **rangeSlider:** handles now support stacking (#1129) ([ad394d3](https://github.com/algolia/instantsearch.js/commit/ad394d3)) * **rangeSlider:** use stats min/max when only user min or max is provided (#1124) ([4348463](https://github.com/algolia/instantsearch.js/commit/4348463)), closes [#1004](https://github.com/algolia/instantsearch.js/issues/1004) * **searchBox:** force cursor position to be at the end of the query (#1123) ([8a27769](https://github.com/algolia/instantsearch.js/commit/8a27769)), closes [#946](https://github.com/algolia/instantsearch.js/issues/946) * **searchBox:** IE8, IE9 needs to listen for setQuery ([97c166a](https://github.com/algolia/instantsearch.js/commit/97c166a)) * **searchBox:** update helper query on every keystroke (#1127) ([997c0c2](https://github.com/algolia/instantsearch.js/commit/997c0c2)), closes [#1015](https://github.com/algolia/instantsearch.js/issues/1015) * **urlSync:** urls should be safe by default (#1104) ([db833c6](https://github.com/algolia/instantsearch.js/commit/db833c6)), closes [#982](https://github.com/algolia/instantsearch.js/issues/982) ## [1.6.1](https://github.com/algolia/instantsearch.js/compare/v1.6.0...v1.6.1) (2016-06-20) ### Bug Fixes * **meteorjs:** lite build must point to the browser lite (#1097) ([265ace3](https://github.com/algolia/instantsearch.js/commit/265ace3)) * **toggle:** read numerical facet results stats for toggle count (#1098) ([1feb539](https://github.com/algolia/instantsearch.js/commit/1feb539)), closes [#1096](https://github.com/algolia/instantsearch.js/issues/1096) * **website:** footer wording ([8355460](https://github.com/algolia/instantsearch.js/commit/8355460)) # [1.6.0](https://github.com/algolia/instantsearch.js/compare/v1.5.2...v1.6.0) (2016-06-13) ### Bug Fixes * **hits:** rename __position to hitIndex ([d051a54](https://github.com/algolia/instantsearch.js/commit/d051a54)) * **refinementList/header:** rename count to refinedFacetCount ([89ad602](https://github.com/algolia/instantsearch.js/commit/89ad602)) ### Features * **header:** Pass count of current refined filters in header ([d9e8582](https://github.com/algolia/instantsearch.js/commit/d9e8582)), closes [#1013](https://github.com/algolia/instantsearch.js/issues/1013) [#1041](https://github.com/algolia/instantsearch.js/issues/1041) * **hits:** Add a `__position` attribute to data passed to items ([43ce1c7](https://github.com/algolia/instantsearch.js/commit/43ce1c7)), closes [#903](https://github.com/algolia/instantsearch.js/issues/903) ## [1.5.2](https://github.com/algolia/instantsearch.js/compare/v1.5.1...v1.5.2) (2016-06-10) ### Bug Fixes * **lite:** use lite algoliasearch build (js client) ([219fa9f](https://github.com/algolia/instantsearch.js/commit/219fa9f)), closes [#1024](https://github.com/algolia/instantsearch.js/issues/1024) * **poweredBy:** Let users define their own poweredBy template ([f1a96d8](https://github.com/algolia/instantsearch.js/commit/f1a96d8)) ## [1.5.1](https://github.com/algolia/instantsearch.js/compare/v1.5.0...v1.5.1) (2016-05-17) ### Bug Fixes * **numericRefinementList:** Correctly apply active class ([7cca9a4](https://github.com/algolia/instantsearch.js/commit/7cca9a4)), closes [#1010](https://github.com/algolia/instantsearch.js/issues/1010) # [1.5.0](https://github.com/algolia/instantsearch.js/compare/v1.4.5...v1.5.0) (2016-04-29) ### Bug Fixes * **base href:** always create absolute URLS in widgets ([ae6dbf6](https://github.com/algolia/instantsearch.js/commit/ae6dbf6)), closes [#970](https://github.com/algolia/instantsearch.js/issues/970) * **IE11:** classList do not supports .add(class, class) ([ab10347](https://github.com/algolia/instantsearch.js/commit/ab10347)), closes [#989](https://github.com/algolia/instantsearch.js/issues/989) * **lifecycle:** save configuration done in widget.init ([07d1fea](https://github.com/algolia/instantsearch.js/commit/07d1fea)) * **RefinementList:** use attributeNameKey when calling createURL ([253ec28](https://github.com/algolia/instantsearch.js/commit/253ec28)) * **rootpath:** remember rootpath option on 'back' button ([01ecdaa](https://github.com/algolia/instantsearch.js/commit/01ecdaa)) * **searchBox:** do not trigger a search when input value is the same ([81c2e80](https://github.com/algolia/instantsearch.js/commit/81c2e80)) * **urlSync:** only start watching for changes at first render ([4a672ae](https://github.com/algolia/instantsearch.js/commit/4a672ae)) ### Features * **urlSync:** allow overriding replaceState(state)/pushState(state) ([989856c](https://github.com/algolia/instantsearch.js/commit/989856c)) ## [1.4.5](https://github.com/algolia/instantsearch.js/compare/v1.4.4...v1.4.5) (2016-04-18) ### Bug Fixes * **showMore:** hide "show less" when nothing to hide ([5ac2bb6](https://github.com/algolia/instantsearch.js/commit/5ac2bb6)) ## [1.4.4](https://github.com/algolia/instantsearch.js/compare/v1.4.3...v1.4.4) (2016-04-15) ### Bug Fixes * **pagination:** Disabled pagination link can no longer be clicked ([88b567f](https://github.com/algolia/instantsearch.js/commit/88b567f)), closes [#974](https://github.com/algolia/instantsearch.js/issues/974) * **showMore:** hide showMore when no more facet values to show ([cc31b1a](https://github.com/algolia/instantsearch.js/commit/cc31b1a)) ## [1.4.3](https://github.com/algolia/instantsearch.js/compare/v1.4.2...v1.4.3) (2016-04-01) ### Bug Fixes * **rangeSlider:** step accepts a float value ([6ecc925](https://github.com/algolia/instantsearch.js/commit/6ecc925)) ## [1.4.2](https://github.com/algolia/instantsearch.js/compare/v1.4.1...v1.4.2) (2016-03-24) ### Performance Improvements * **refinementList:** Stop creating URL for hidden refinements. ([2cdd17d](https://github.com/algolia/instantsearch.js/commit/2cdd17d)) ## [1.4.1](https://github.com/algolia/instantsearch.js/compare/v1.4.0...v1.4.1) (2016-03-22) ### Bug Fixes * **searchBox:** do not update the input when focused ([61cf9be](https://github.com/algolia/instantsearch.js/commit/61cf9be)), closes [#944](https://github.com/algolia/instantsearch.js/issues/944) # [1.4.0](https://github.com/algolia/instantsearch.js/compare/v1.3.3...v1.4.0) (2016-03-16) ### Bug Fixes * **url:** allow hierarchical facets in trackedParameters ([36b4011](https://github.com/algolia/instantsearch.js/commit/36b4011)) ### Features * **url-sync:** use the new mapping option ([f869885](https://github.com/algolia/instantsearch.js/commit/f869885)), closes [#838](https://github.com/algolia/instantsearch.js/issues/838) ## [1.3.3](https://github.com/algolia/instantsearch.js/compare/v1.3.2...v1.3.3) (2016-03-07) ### Bug Fixes * **headerFooter:** make collapsible click handler work ([add0d50](https://github.com/algolia/instantsearch.js/commit/add0d50)) ### Performance Improvements * **linters:** Greatly improve the `npm run lint` task speed ([1ba53b0](https://github.com/algolia/instantsearch.js/commit/1ba53b0)) ## [1.3.2](https://github.com/algolia/instantsearch.js/compare/v1.3.1...v1.3.2) (2016-03-07) ### Bug Fixes * **Template:** stop leaking `data="[object Object]"` attributes in production builds ([7ec0431](https://github.com/algolia/instantsearch.js/commit/7ec0431)), closes [#899](https://github.com/algolia/instantsearch.js/issues/899) ### Features * **validate-pr:** Allow `docs()` commits to be merged in master ([0abc689](https://github.com/algolia/instantsearch.js/commit/0abc689)) ## [1.3.1](https://github.com/algolia/instantsearch.js/compare/v1.3.0...v1.3.1) (2016-03-07) ### Bug Fixes * **collapsible:** stop duplicating collapsible styling ([7362901](https://github.com/algolia/instantsearch.js/commit/7362901)) * **lodash:** stop leaking lodash in the global scope ([91f71dc](https://github.com/algolia/instantsearch.js/commit/91f71dc)), closes [#900](https://github.com/algolia/instantsearch.js/issues/900) # [1.3.0](https://github.com/algolia/instantsearch.js/compare/v1.2.5...v1.3.0) (2016-03-04) ### Bug Fixes * **browser support:** make IE lte 10 work by fixing Object.getPrototypeOf ([bbb264b](https://github.com/algolia/instantsearch.js/commit/bbb264b)) * **menu,refinementList:** sort by count AND name to avoid reorders on refine ([02fe7bf](https://github.com/algolia/instantsearch.js/commit/02fe7bf)), closes [#65](https://github.com/algolia/instantsearch.js/issues/65) * **priceRanges:** pass the bound refine to the form ([ce2b956](https://github.com/algolia/instantsearch.js/commit/ce2b956)) * **searchBox:** handle external updates of the query ([6a0af14](https://github.com/algolia/instantsearch.js/commit/6a0af14)), closes [#803](https://github.com/algolia/instantsearch.js/issues/803) * **searchBox:** stop setting the query twice ([91270b2](https://github.com/algolia/instantsearch.js/commit/91270b2)) * **searchBox:** stop updating query at eachkeystroke with searchOnEnterKeyPressOnly ([28dc4d2](https://github.com/algolia/instantsearch.js/commit/28dc4d2)), closes [#875](https://github.com/algolia/instantsearch.js/issues/875) * **Slider:** do not render Slider when range.min === range.max ([f20274e](https://github.com/algolia/instantsearch.js/commit/f20274e)) * **Template:** now render() when templateKey changes ([8906224](https://github.com/algolia/instantsearch.js/commit/8906224)) * **toggle:** pass isRefined to toggleRefinement ([8ac494e](https://github.com/algolia/instantsearch.js/commit/8ac494e)) * **url-sync:** always decode incoming query string ([bea38e3](https://github.com/algolia/instantsearch.js/commit/bea38e3)), closes [#848](https://github.com/algolia/instantsearch.js/issues/848) * **url-sync:** handle href pages ([e58aadc](https://github.com/algolia/instantsearch.js/commit/e58aadc)), closes [#790](https://github.com/algolia/instantsearch.js/issues/790) ### Features * **collapsable widgets:** add collapsable and collapsed option ([c4df7c5](https://github.com/algolia/instantsearch.js/commit/c4df7c5)) * **instantsearch:** allow overriding the helper.search function ([9a930e7](https://github.com/algolia/instantsearch.js/commit/9a930e7)) * **rangeSlider:** allow passing min and max values ([409295c](https://github.com/algolia/instantsearch.js/commit/409295c)), closes [#858](https://github.com/algolia/instantsearch.js/issues/858) * **searchBox:** allow to pass a queryHook ([5786a64](https://github.com/algolia/instantsearch.js/commit/5786a64)) * **Template:** allow template functions to return a React element ([748077d](https://github.com/algolia/instantsearch.js/commit/748077d)) * **Template:** allow template functions to return a React element ([0f9296d](https://github.com/algolia/instantsearch.js/commit/0f9296d)) ### Performance Improvements * **autoHideContainer:** stop re-creating React components ([8c89862](https://github.com/algolia/instantsearch.js/commit/8c89862)) * **formatting numbers:** stop using a default locale, use the system one ([b056554](https://github.com/algolia/instantsearch.js/commit/b056554)) * **nouislider:** upgrade nouislider, shaves some more ms ([fefbe65](https://github.com/algolia/instantsearch.js/commit/fefbe65)) * **React:** use babel `optimisation` option for React ([95f940c](https://github.com/algolia/instantsearch.js/commit/95f940c)) * **React, widgets:** implement shouldComponentUpdate, reduce bind ([5efaac1](https://github.com/algolia/instantsearch.js/commit/5efaac1)) ## [1.2.5](https://github.com/algolia/instantsearch.js/compare/v1.2.4...v1.2.5) (2016-03-02) ### Bug Fixes * **hierarchicalMenu:** configure maxValuesPerFacet using the limit option ([4868717](https://github.com/algolia/instantsearch.js/commit/4868717)), closes [#66](https://github.com/algolia/instantsearch.js/issues/66) ## [1.2.4](https://github.com/algolia/instantsearch.js/compare/v1.2.3...v1.2.4) (2016-02-29) Upgraded the helper to 2.9.0 to support undocumented parameters from the API. ## [1.2.3](https://github.com/algolia/instantsearch.js/compare/v1.2.2...v1.2.3) (2016-02-18) ### Bug Fixes * **currentRefinedValues:** clear numeric refinements using original value ([9a0ad45](https://github.com/algolia/instantsearch.js/commit/9a0ad45)), closes [#844](https://github.com/algolia/instantsearch.js/issues/844) ## [1.2.2](https://github.com/algolia/instantsearch.js/compare/v1.2.1...v1.2.2) (2016-02-03) ### Features * **menu:** add showMore option ([e7e7677](https://github.com/algolia/instantsearch.js/commit/e7e7677)), closes [#815](https://github.com/algolia/instantsearch.js/issues/815) ## [1.2.1](https://github.com/algolia/instantsearch.js/compare/v1.2.0...v1.2.1) (2016-02-02) ### Bug Fixes * **showmore:** now showMore in doc and also show-more BEM ([a020439](https://github.com/algolia/instantsearch.js/commit/a020439)) # [1.2.0](https://github.com/algolia/instantsearch.js/compare/v1.1.3...v1.2.0) (2016-02-02) ### Bug Fixes * **all:** typos ([fa8ba09](https://github.com/algolia/instantsearch.js/commit/fa8ba09)) * **currentRefinedValues:** allow array of strings for cssClasses.* ([55b3a3f](https://github.com/algolia/instantsearch.js/commit/55b3a3f)) * **docs:** fixed bad link to scss in custom themes section ([823a859](https://github.com/algolia/instantsearch.js/commit/823a859)) * **getRefinements:** a name should be a string ([7efd1fd](https://github.com/algolia/instantsearch.js/commit/7efd1fd)) * **getRefinements:** hierarchical facets ([fe0fc5d](https://github.com/algolia/instantsearch.js/commit/fe0fc5d)) * **index:** Use module.exports instead of export on index ([81e7eee](https://github.com/algolia/instantsearch.js/commit/81e7eee)) * **pagination:** remove default value of maxPages. Fixes #761 ([607fe9a](https://github.com/algolia/instantsearch.js/commit/607fe9a)), closes [#761](https://github.com/algolia/instantsearch.js/issues/761) * **prepareTemplates:** uses templates with keys that are not in defaults ([c4bf8ec](https://github.com/algolia/instantsearch.js/commit/c4bf8ec)) * **rangeSlider:** prevent slider from extending farther than the last pip ([6e534f5](https://github.com/algolia/instantsearch.js/commit/6e534f5)) * **search-box:** update value when state changes from the outside ([4550f99](https://github.com/algolia/instantsearch.js/commit/4550f99)) * **url-sync:** adds indexName in the helper configuration ([e50bafd](https://github.com/algolia/instantsearch.js/commit/e50bafd)) * **url-sync:** Makes url sync more reliable ([3157abc](https://github.com/algolia/instantsearch.js/commit/3157abc)), closes [#730](https://github.com/algolia/instantsearch.js/issues/730) [#729](https://github.com/algolia/instantsearch.js/issues/729) ### Features * **currentRefinedValues:** new widget ([6c926d0](https://github.com/algolia/instantsearch.js/commit/6c926d0)), closes [#404](https://github.com/algolia/instantsearch.js/issues/404) * **hits:** adds allItems template as an alternative to item ([1f3f889](https://github.com/algolia/instantsearch.js/commit/1f3f889)) * **poweredBy:** automatically add utm link to poweredBy ([05d1425](https://github.com/algolia/instantsearch.js/commit/05d1425)), closes [#711](https://github.com/algolia/instantsearch.js/issues/711) * **priceRanges:** add currency option ([f41484a](https://github.com/algolia/instantsearch.js/commit/f41484a)) * **refinementlist:** lets configure showmore feature ([3b8688a](https://github.com/algolia/instantsearch.js/commit/3b8688a)) * **Template:** accepts any parameters and forwards them ([5170f53](https://github.com/algolia/instantsearch.js/commit/5170f53)) ## [1.1.3](https://github.com/algolia/instantsearch.js/compare/v1.1.2...v1.1.3) (2016-01-12) ### Bug Fixes * **searchBox:** fixes cssClasses option ([660ee2f](https://github.com/algolia/instantsearch.js/commit/660ee2f)), closes [#775](https://github.com/algolia/instantsearch.js/issues/775) ## [1.1.2](https://github.com/algolia/instantsearch.js/compare/v1.1.1...v1.1.2) (2016-01-08) ## [1.1.1](https://github.com/algolia/instantsearch.js/compare/v1.1.0...v1.1.1) (2016-01-07) ### Bug Fixes * **style:** keyframes ([40eb0a5](https://github.com/algolia/instantsearch.js/commit/40eb0a5)) * **url-sync:** adds indexName in the helper configuration ([c2c0bc7](https://github.com/algolia/instantsearch.js/commit/c2c0bc7)) ### Features * **clearRefinements:** Added two utils methods ([49564e1](https://github.com/algolia/instantsearch.js/commit/49564e1)) # [1.1.0](https://github.com/algolia/instantsearch.js/compare/v1.0.0...v1.1.0) (2015-11-26) ### Bug Fixes * **pagination:** fix #668 edge case ([d8f1196](https://github.com/algolia/instantsearch.js/commit/d8f1196)), closes [#668](https://github.com/algolia/instantsearch.js/issues/668) * **priceRanges:** Remove round from first range ([bf82395](https://github.com/algolia/instantsearch.js/commit/bf82395)) * **slider:** hide the slider when stats.min=stats.max ([42e4b64](https://github.com/algolia/instantsearch.js/commit/42e4b64)) * **starRating:** Retrieve the correct count and use numericRefinement ([f00ce38](https://github.com/algolia/instantsearch.js/commit/f00ce38)), closes [#615](https://github.com/algolia/instantsearch.js/issues/615) ### Features * **hierarchical:** expose rootPath and showParentLevel ([6e9bb7c](https://github.com/algolia/instantsearch.js/commit/6e9bb7c)) # [1.0.0](https://github.com/algolia/instantsearch.js/compare/v0.14.9...v1.0.0) (2015-11-18) ## [0.14.9](https://github.com/algolia/instantsearch.js/compare/v0.14.8...v0.14.9) (2015-11-18) ## [0.14.8](https://github.com/algolia/instantsearch.js/compare/v0.14.7...v0.14.8) (2015-11-18) ## [0.14.7](https://github.com/algolia/instantsearch.js/compare/v0.14.6...v0.14.7) (2015-11-18) ## [0.14.6](https://github.com/algolia/instantsearch.js/compare/v0.14.5...v0.14.6) (2015-11-17) ## [0.14.5](https://github.com/algolia/instantsearch.js/compare/v0.14.4...v0.14.5) (2015-11-17) ## [0.14.4](https://github.com/algolia/instantsearch.js/compare/v0.14.3...v0.14.4) (2015-11-17) ### Bug Fixes * **doc:** Expand input on documentation page ([6814a14](https://github.com/algolia/instantsearch.js/commit/6814a14)) ## [0.14.3](https://github.com/algolia/instantsearch.js/compare/v0.14.2...v0.14.3) (2015-11-17) ### Bug Fixes * **examples:** media logo ([64f850e](https://github.com/algolia/instantsearch.js/commit/64f850e)) * **website:** demos link to https ([b69c0f5](https://github.com/algolia/instantsearch.js/commit/b69c0f5)) ## [0.14.2](https://github.com/algolia/instantsearch.js/compare/v0.14.1...v0.14.2) (2015-11-17) ### Bug Fixes * **numericSelector:** pass currentValue as the refined value, not the full obj ([9286b4b](https://github.com/algolia/instantsearch.js/commit/9286b4b)) * **website:** search icon ([623f071](https://github.com/algolia/instantsearch.js/commit/623f071)) ## [0.14.1](https://github.com/algolia/instantsearch.js/compare/v0.14.0...v0.14.1) (2015-11-16) ### Bug Fixes * **docs:** minor CSS fixes ([94fa868](https://github.com/algolia/instantsearch.js/commit/94fa868)), closes [#573](https://github.com/algolia/instantsearch.js/issues/573) # [0.14.0](https://github.com/algolia/instantsearch.js/compare/v0.13.0...v0.14.0) (2015-11-13) ### Bug Fixes * **hierarchicalMenu:** handle limit option ([968cf58](https://github.com/algolia/instantsearch.js/commit/968cf58)), closes [#585](https://github.com/algolia/instantsearch.js/issues/585) [#235](https://github.com/algolia/instantsearch.js/issues/235) * **numeric-selector:** makes init comply with the new API ([068e8d3](https://github.com/algolia/instantsearch.js/commit/068e8d3)) ### Features * **core:** sends a custom User Agent ([2561154](https://github.com/algolia/instantsearch.js/commit/2561154)) * **lifecycle:** makes init API consistent with the rest ([e7ed81f](https://github.com/algolia/instantsearch.js/commit/e7ed81f)) ### BREAKING CHANGES * all widgets using "facetName" are now using "attributeName" # [0.13.0](https://github.com/algolia/instantsearch.js/compare/v0.12.3...v0.13.0) (2015-11-12) ### Features * **clearAll:** New widget ([9e61a14](https://github.com/algolia/instantsearch.js/commit/9e61a14)) ## [0.12.3](https://github.com/algolia/instantsearch.js/compare/v0.12.2...v0.12.3) (2015-11-12) ## [0.12.2](https://github.com/algolia/instantsearch.js/compare/v0.12.1...v0.12.2) (2015-11-12) ### Bug Fixes * **layout:** missing div (did we lost that fix?) ([9a515e4](https://github.com/algolia/instantsearch.js/commit/9a515e4)) ## [0.12.1](https://github.com/algolia/instantsearch.js/compare/v0.12.0...v0.12.1) (2015-11-12) ### Bug Fixes * **counts:** missing formatNumber calls ([65e5ba0](https://github.com/algolia/instantsearch.js/commit/65e5ba0)), closes [#560](https://github.com/algolia/instantsearch.js/issues/560) * **doc:** ensure selector is not conflicting ([6528f2c](https://github.com/algolia/instantsearch.js/commit/6528f2c)), closes [#505](https://github.com/algolia/instantsearch.js/issues/505) * **docs:** improved label/input hover debug ([58573db](https://github.com/algolia/instantsearch.js/commit/58573db)), closes [#503](https://github.com/algolia/instantsearch.js/issues/503) * **examples/airbnb:** Use default theme from CDN ([f379c0a](https://github.com/algolia/instantsearch.js/commit/f379c0a)), closes [#522](https://github.com/algolia/instantsearch.js/issues/522) * **examples/youtube:** use the default theme ([cf9a4b6](https://github.com/algolia/instantsearch.js/commit/cf9a4b6)) * **rangeSlider:** fixed tooltip CSS & outdated default theme. ([c4be2ef](https://github.com/algolia/instantsearch.js/commit/c4be2ef)) # [0.12.0](https://github.com/algolia/instantsearch.js/compare/v0.11.1...v0.12.0) (2015-11-10) ### Bug Fixes * **pagination:** Fix double BEM classes on elements ([2ede317](https://github.com/algolia/instantsearch.js/commit/2ede317)), closes [#500](https://github.com/algolia/instantsearch.js/issues/500) * **price-ranges:** fix usage + add test ([89601d7](https://github.com/algolia/instantsearch.js/commit/89601d7)) * **range-slider:** check usage + display (fixes #395) ([301643a](https://github.com/algolia/instantsearch.js/commit/301643a)), closes [#395](https://github.com/algolia/instantsearch.js/issues/395) * **rangeSlider:** error when no result ([70e8554](https://github.com/algolia/instantsearch.js/commit/70e8554)) * **theme:** Revert default spacing into pagination ([d755fd5](https://github.com/algolia/instantsearch.js/commit/d755fd5)) ### BREAKING CHANGES * pagination: Removes all `__disabled`, `__first`, `__last`, `__next`, `__previous`, `__active` and `__page` classes added on the links in the pagination. It only ads them to the parent `li`. Links instead now have a `.ais-pagination--link` class Previously, the same CSS classes where added to both the `item` (`li`) and the link inside it. I've split them in `--item` and `--link`. I've also made the various active/first/disabled/etc modifiers as actual `__modifier` classes. I've updated the tests, the CSS skeleton, the examples and the docs accordingly. ## [0.11.1](https://github.com/algolia/instantsearch.js/compare/v0.11.0...v0.11.1) (2015-11-10) # [0.11.0](https://github.com/algolia/instantsearch.js/compare/v0.10.0...v0.11.0) (2015-11-06) ### Bug Fixes * **bem:** Make scss mixins actually follow BEM ([fcfb408](https://github.com/algolia/instantsearch.js/commit/fcfb408)) * **doc:** bolder font for the navigation ([64f6d56](https://github.com/algolia/instantsearch.js/commit/64f6d56)) * **InstantSearch:** throw error when init and render are not defined. Fixes #499 ([2830cd3](https://github.com/algolia/instantsearch.js/commit/2830cd3)), closes [#499](https://github.com/algolia/instantsearch.js/issues/499) * **live-doc:** adds a start at a responsive display ([c83967e](https://github.com/algolia/instantsearch.js/commit/c83967e)) * **live-doc:** adds navigation menu for smaller screens ([a6bb71e](https://github.com/algolia/instantsearch.js/commit/a6bb71e)) * **live-doc:** fixes flow for texts ([3855071](https://github.com/algolia/instantsearch.js/commit/3855071)) * **live-doc:** Momentum scroll for iPhone ([60a36ff](https://github.com/algolia/instantsearch.js/commit/60a36ff)) * **live-doc:** uses only h4 and fixes style of h4 (mobile) ([0fdd2d0](https://github.com/algolia/instantsearch.js/commit/0fdd2d0)) * **middle-click:** Allow middle click on links ([a7601c0](https://github.com/algolia/instantsearch.js/commit/a7601c0)) * **range-slider:** Use lodash find instead of Array.prototype.find ([056153c](https://github.com/algolia/instantsearch.js/commit/056153c)) * **searchBox:** handling pasting event with contextual menu. ([a172458](https://github.com/algolia/instantsearch.js/commit/a172458)), closes [#467](https://github.com/algolia/instantsearch.js/issues/467) * **website:** defered doc scripts ([0c1324f](https://github.com/algolia/instantsearch.js/commit/0c1324f)) * **website:** doc layout responsive ([a4dc894](https://github.com/algolia/instantsearch.js/commit/a4dc894)) * **website:** fixed space overlay color animation ([200b8a7](https://github.com/algolia/instantsearch.js/commit/200b8a7)) * **website:** Fixes & responsive stuff for doc ([7a8f920](https://github.com/algolia/instantsearch.js/commit/7a8f920)) * **website:** footer markup ([95364a1](https://github.com/algolia/instantsearch.js/commit/95364a1)) * **website:** home.js lint ([b70e06e](https://github.com/algolia/instantsearch.js/commit/b70e06e)) * **website:** icon-theme didn't like svgo (to fix) ([38d84af](https://github.com/algolia/instantsearch.js/commit/38d84af)) * **website:** image alt ([30cca29](https://github.com/algolia/instantsearch.js/commit/30cca29)) * **website:** jsdelivr for every scripts ([06591d4](https://github.com/algolia/instantsearch.js/commit/06591d4)) * **website:** Nav Icon + logo ([c1f419c](https://github.com/algolia/instantsearch.js/commit/c1f419c)) * **website:** only load what's needed in bootstrap ([4843474](https://github.com/algolia/instantsearch.js/commit/4843474)) * **website:** removed animation debug ([01ac079](https://github.com/algolia/instantsearch.js/commit/01ac079)) * **website:** space bg fadeIn ([5e09844](https://github.com/algolia/instantsearch.js/commit/5e09844)) * **website:** unclosed content block ([d42dc3e](https://github.com/algolia/instantsearch.js/commit/d42dc3e)) ### Features * **hierarchicalMenu:** Adding indentation with default theme ([34885d2](https://github.com/algolia/instantsearch.js/commit/34885d2)) ### BREAKING CHANGES * hierarchicalMenu: Hierarchical menu levels 1 and 2 now have a margin-left added in the default theme. # [0.10.0](https://github.com/algolia/instantsearch.js/compare/v0.9.0...v0.10.0) (2015-11-06) ### Bug Fixes * **api:** rename hideContainerWhenNoResults to autoHideContainer ([3f64bef](https://github.com/algolia/instantsearch.js/commit/3f64bef)), closes [#407](https://github.com/algolia/instantsearch.js/issues/407) * **doc:** ensure the documentation content doesn't overflow ([1e28a4e](https://github.com/algolia/instantsearch.js/commit/1e28a4e)), closes [#444](https://github.com/algolia/instantsearch.js/issues/444) * **hitsPerPageSelector:** Be more tolerant in options ([e14a344](https://github.com/algolia/instantsearch.js/commit/e14a344)) * **numeric widgets:** synchronizes rounded value between widgets ([b314160](https://github.com/algolia/instantsearch.js/commit/b314160)) * **numeric-refinement:** Replace Array.find with lodash find/includes ([b3e815c](https://github.com/algolia/instantsearch.js/commit/b3e815c)) * **price-ranges:** makes it uses same operator as the slider ([ad6f5c2](https://github.com/algolia/instantsearch.js/commit/ad6f5c2)) * **range-slider:** fixes bound definition ([e15c9b7](https://github.com/algolia/instantsearch.js/commit/e15c9b7)) * **selector:** makes component as uncontrolled component ([1dda12a](https://github.com/algolia/instantsearch.js/commit/1dda12a)) * **slider:** fixed `pip` propTypes constraint ([c77b7f4](https://github.com/algolia/instantsearch.js/commit/c77b7f4)) * **website:** fix images path ([a3f62eb](https://github.com/algolia/instantsearch.js/commit/a3f62eb)) ### Features * **searchBox:** ability to be non-instant ([b3ef871](https://github.com/algolia/instantsearch.js/commit/b3ef871)), closes [#458](https://github.com/algolia/instantsearch.js/issues/458) * **toggle:** Allow custom on/off values ([9b6c2bf](https://github.com/algolia/instantsearch.js/commit/9b6c2bf)), closes [#409](https://github.com/algolia/instantsearch.js/issues/409) ### Performance Improvements * **hitsPerPageSelector:** Use the correct lodash function ([be9aea7](https://github.com/algolia/instantsearch.js/commit/be9aea7)) ### BREAKING CHANGES * api: use autoHideContainer instead of hideContainerWhenNoResults # [0.9.0](https://github.com/algolia/instantsearch.js/compare/v0.8.2...v0.9.0) (2015-11-04) ### Features * **numericRefinementList:** create numericRefinementList widget using refinementList component ([a29e9c7](https://github.com/algolia/instantsearch.js/commit/a29e9c7)) ## [0.8.2](https://github.com/algolia/instantsearch.js/compare/v0.8.1...v0.8.2) (2015-11-04) ### Bug Fixes * **doc:** All wigdets in docs are not anymore linked together #fix #446 ([4361320](https://github.com/algolia/instantsearch.js/commit/4361320)), closes [#446](https://github.com/algolia/instantsearch.js/issues/446) * **hitsPerPageSelector:** Issue when state did not have a `hitsPerPage` ([dc9371c](https://github.com/algolia/instantsearch.js/commit/dc9371c)) ## [0.8.1](https://github.com/algolia/instantsearch.js/compare/v0.8.0...v0.8.1) (2015-11-04) ### Bug Fixes * **hierarchicalMenu:** handle cases where no results after a search ([0a1d0ac](https://github.com/algolia/instantsearch.js/commit/0a1d0ac)), closes [#385](https://github.com/algolia/instantsearch.js/issues/385) ### Features * **build:** allow building React based custom widgets ([cfbbfe4](https://github.com/algolia/instantsearch.js/commit/cfbbfe4)), closes [#373](https://github.com/algolia/instantsearch.js/issues/373) # [0.8.0](https://github.com/algolia/instantsearch.js/compare/v0.7.0...v0.8.0) (2015-11-03) ### Bug Fixes * **cssClasses:** Fixed duplication of classNames ([e193f45](https://github.com/algolia/instantsearch.js/commit/e193f45)), closes [#388](https://github.com/algolia/instantsearch.js/issues/388) * **doc:** add doctype were missing ([86a18aa](https://github.com/algolia/instantsearch.js/commit/86a18aa)) * **doc:** new color scheme ([deccc17](https://github.com/algolia/instantsearch.js/commit/deccc17)) * **doc:** only show a scrollbar when needed ([f2d955b](https://github.com/algolia/instantsearch.js/commit/f2d955b)) * **hierarchical:** setPage 0 when toggling ([a976539](https://github.com/algolia/instantsearch.js/commit/a976539)), closes [#371](https://github.com/algolia/instantsearch.js/issues/371) * **jsdoc:** use babel-node ([453dc21](https://github.com/algolia/instantsearch.js/commit/453dc21)) * **live-doc:** generates missing ul ([b43e6e2](https://github.com/algolia/instantsearch.js/commit/b43e6e2)) * **live-doc:** move scrollbars, removes useless ones ([548ae5f](https://github.com/algolia/instantsearch.js/commit/548ae5f)) * **live-doc:** moves octocat link to top. Removes stackOverflow ([8ff6a79](https://github.com/algolia/instantsearch.js/commit/8ff6a79)) * **live-doc:** Moves version in the main content ([27731c3](https://github.com/algolia/instantsearch.js/commit/27731c3)) * **live-reload:** integrates the links into the menu flow ([c118051](https://github.com/algolia/instantsearch.js/commit/c118051)) * **numerical widgets:** s/facetName/attributeName ([f209f5d](https://github.com/algolia/instantsearch.js/commit/f209f5d)), closes [#431](https://github.com/algolia/instantsearch.js/issues/431) * **refinementList:** ensure the key reflects the underlying state ([b048f0b](https://github.com/algolia/instantsearch.js/commit/b048f0b)), closes [#398](https://github.com/algolia/instantsearch.js/issues/398) ### Features * **examples:** try examples instead of themes ([bedffce](https://github.com/algolia/instantsearch.js/commit/bedffce)) * **headerFooter:** Only add markup if a template is defined ([7a2d22d](https://github.com/algolia/instantsearch.js/commit/7a2d22d)), closes [#370](https://github.com/algolia/instantsearch.js/issues/370) * **priceRanges:** Add BEM classes and tests ([ad58d7a](https://github.com/algolia/instantsearch.js/commit/ad58d7a)), closes [#387](https://github.com/algolia/instantsearch.js/issues/387) ### BREAKING CHANGES * numerical widgets: the priceRanges and rangeSlider widgets are now using `attributeName` instead of `facetName`. * priceRanges: `ais-price-ranges--range` are now named `ais-price-ranges--item` and are wrapped in a `ais-price-ranges--list`. I've moved the bottom form into it's own PriceRangesForm component, along with its own tests. I've fixed a minor typo where the component was internally named PriceRange (without the final __s__). I factorize some logic form the render in individual methods and manage to individually test them. This was not an easy task. I had to mock the default `render` (so it does nothing) before instantiating the component. Then, I was able to call each inner method individually. This requires to stub prototype methods in beforeEach, then restore them in afterEach. I've added a few helper methods, this can surely be simplified again but this gives nice granularity in testing. I've renamed the `range` items to `item` and wrapped them in a `list`. I've also added classes to all elements we add (`label`, `separator`, etc). I've removed the empty `span`s. * headerFooter: The `
` and `