UNPKG

search-client

Version:

Javascript library for executing searches in the Haive search-index via the SearchManager REST interface.

801 lines 34.8 kB
import { __assign } from "tslib"; import clone from 'clone'; import deepEqual from 'deep-equal'; import { AuthenticationFactory, } from './Authentication'; import { Autocomplete } from './Autocomplete'; import { Categorize } from './Categorize'; import { CategoryPresentation, Query, } from './Common'; import { Find } from './Find'; import { Settings } from './Settings'; export * from './Authentication'; export * from './Autocomplete'; export * from './Categorize'; export * from './Common'; export * from './Data'; export * from './Find'; export * from './Settings'; /** * This is the "main class" of this package. Please read the <a href="https://the-haive.github.io/search-client/">getting-started section</a>" * for a proper introduction. * * The SearchClient manages a range of other services: * * Authentication, * * Autocomplete, * * Categorize * * Find * * Each of the above services can be used independently, but it is highly recommended to use the SearchClient instead. * * The SearchClient allows you to have an advanced search with minimal effort in regards to setup and logics. instead * of having to write all the logics yourself the SearchClient exposes the following methods for managing your search: * 1. Configure callbacks in your settings-object that you pass to the SearchClient. * 2. Configure triggers to define when to do server-lookups and not (if you need to deviate from the defaults) * 3. Set query-values real-time (queryText, filters, date-ranges, etc.) * 4. Receive autocomplete-suggestions, matches and categories in your callback handlers when the data is available. * * What happens is that any query-changes that arrive are checked in regards to trigger-settings. If they are to trigger * and a callback has been set up then the server is requested and when the data is received it is sent to the callback * registered in the settings-object. */ var SearchClient = /** @class */ (function () { /** * Creates a SearchClient instance using the supplied settings object. Please see <a href="https://the-haive.github.io/search-client/">getting-started section</a> * for an introduction on how to set up the instance. * * @param settings A settings object that indicates how the search-client instance is to behave. */ function SearchClient(settings, fetchMethod) { /** * Holds a reference to the setup Authentication service. */ this.authentication = undefined; /** * Holds a reference to the setup Autocomplete service. */ this.autocomplete = undefined; /** * Holds a reference to the setup Categorize service. */ this.categorize = undefined; /** * Holds a reference to the setup Find service. */ this.find = undefined; this.setup(settings, fetchMethod); this.tokenResolver = function () { return ''; }; } Object.defineProperty(SearchClient.prototype, "authenticationToken", { /** * Holds a reference to the currently set authentication token. */ get: function () { return this.tokenResolver(); }, enumerable: false, configurable: true }); /** * This method is typically called when the user clicks the search-button in the UI. * * For query-fields that accepts enter the default queryChangeInstantRegex catches enter. * When they don't take enter you will have to set up something that either catches the default enter or a user clicks * on a "Search"-button or similar. You can choose to use the already current query, or you can pass it in. If you * include the query then the internal updates are suppressed while changing the query-properties, to make sure that * only one update per service is made (if any of their trigger-checks returned true). * * @param query If passed in then the query object will update the internal query-object and any updates will trigger * (but only once). The consecutive overriding service params are ignored when this parameter has a value. If the * query is empty/null/undefined then the services will force an update, but allows the bool params to override this. * @param autocomplete Allows turning off updates for the Autocomplete service (if the service is enabled in the * settings). Only effective when query is not set. * @param categorize Allows turning off updates for the Categorize service (if the service is enabled in the settings). * Only effective when query is not set. * @param find Allows turning off updates for the Find service (if the service is enabled in the settings). Only * effective when query is not set. */ SearchClient.prototype.update = function (query, autocomplete, categorize, find) { if (autocomplete === void 0) { autocomplete = true; } if (categorize === void 0) { categorize = true; } if (find === void 0) { find = true; } if (query != null) { // Update query without triggering any updates. this.deferUpdates(true); this.query = query; // Turning of deferredUpdates will now execute pending updates (if any). this.deferUpdates(false); // A query was included, so the above update is all we want to do. return; } // Since a query was not passed, then we update based on each service's setting, using the bool params autocomplete, // categorize and find to allow overriding and turning off the individual services. if (autocomplete && this.autocomplete.shouldUpdate()) { this.autocomplete.update(this.query); } if (categorize && this.categorize.shouldUpdate()) { this.categorize.update(this.query); } if (find && this.find.shouldUpdate()) { this.find.update(this.query); } }; /** * This method is called when you want to force an update call to be made for the services. * * It may force an update based on the existing this.query value or you can provide a new query object to be used. * After having set the value the services will be called, unless they are disabled in their respective configs * or turned off in the params to this method. * * @param query If passed in then the query object will update the internal query-object without triggering any updates, * but will just after this force an update on all enabled services, that are not turned off by the consecutive params. * @param autocomplete Allows turning off updates for the Autocomplete service (if the service is enabled in the * settings). * @param categorize Allows turning off updates for the Categorize service (if the service is enabled in the settings). * @param find Allows turning off updates for the Find service (if the service is enabled in the settings). */ SearchClient.prototype.forceUpdate = function (query, autocomplete, categorize, find) { if (autocomplete === void 0) { autocomplete = true; } if (categorize === void 0) { categorize = true; } if (find === void 0) { find = true; } if (query != null) { // Update query without triggering any updates. this.deferUpdates(true); this.query = query; // Skip executing any potential pending updates. this.deferUpdates(false, true); } // Force an update (by passing null to query param) and forwarding service overrides. this.update(null, autocomplete, categorize, find); }; /** * Resets the SearchClient instance (filters, queryText, categoryPresentations++) to initial values. */ SearchClient.prototype.reset = function () { this.setup(this._origSettings, this._origFetchMethod); }; /** * Returns true if the passed argument is a filter. * Typically used to visually indicate that a category is also a filter. */ SearchClient.prototype.isFilter = function (category) { return this._query.isFilter(category); }; /** * Checks whether any child-node of the given category has a filter defined for it. * Typically used to visually show in the tree that a child-node has an active filter. */ SearchClient.prototype.hasChildFilter = function (category) { return this._query.hasChildFilter(category); }; /** * Add the given filter, if it isn't already there. * * Will run trigger-checks and potentially update services. */ SearchClient.prototype.filterAdd = function (filter) { var item = this._query.filterId(filter); var foundIndex = this._query.filterIndex(item); if (foundIndex === -1) { this.doFilterAdd(item); return true; } // Filter already set return false; }; /** * Remove the given filter, if it is already set. * * Will run trigger-checks and potentially update services. */ SearchClient.prototype.filterRemove = function (filter) { var item = this._query.filterId(filter); var foundIndex = this._query.filterIndex(item); if (foundIndex > -1) { this.doFilterRemove(foundIndex); return true; } // Filter already set return false; }; /** * Toggle the given filter. * * Will run trigger-checks and potentially update services. * * @param filter Is either string[], Filter or Category. When string array it expects the equivalent of the Category.categoryName property, which is like this: ["Author", "Normann"]. * @return true if the filter was added, false if it was removed. */ SearchClient.prototype.filterToggle = function (filter) { var item = this._query.filterId(filter); var foundIndex = this._query.filterIndex(item); if (foundIndex > -1) { this.doFilterRemove(foundIndex); return false; } else { this.doFilterAdd(item); return true; } }; /** * Toggles the expansion/collapsed state for the given group/category * * @param node The node that is to be expanded. * @return The new state of the node. */ SearchClient.prototype.toggleCategoryExpansion = function (node, state) { // Look up internal expansion-override list and see if we are already overriding this setting. if (Array.isArray(node)) { node = this.findCategory(node); } var key = node.hasOwnProperty('categoryName') ? node.categoryName.join('|') : node.name; if (!this.settings.categorize.presentations[key]) { this.settings.categorize.presentations[key] = new CategoryPresentation({ expanded: !node.expanded }); } else { this.settings.categorize.presentations[key].expanded = !this .settings.categorize.presentations[key].expanded; } this.categorize.clientCategoriesUpdate(this.query); return this.settings.categorize.presentations[key].expanded; }; /** * Decides whether an update should be executed or not. Typically used to temporarily turn * off update-execution. When turned back on the second param can be used to indicate whether * pending updates should be executed or not. * * **Note:** Changes deferring of updates for all components (Autocomplete, Categorize and Find). * Use the service properties of the SearchClient instance to control deferring for each service. * * @example Some examples: * * // Example 1: Defer updates to avoid multiple updates: * searchClient.deferUpdates(true); * * // Example 2: Change some props that triggers may be listening for * searchClient.dateFrom = { M: -1}; * searchClient.dateTo = { M: 0}; * // When calling deferUpdates with (false) the above two update-events are now executed as one instead (both value-changes are accounted for though) * searchClient.deferUpdates(false); * * // Example 3: Suppress updates (via deferUpdates): * searchClient.deferUpdates(true); * // Change a prop that should trigger updates * searchClient.queryText = "some text"; * // Call deferUpdates with (false, true), to skip the pending update. * searchClient.deferUpdates(false, true); * * // Example 4: Defer update only for one service (Categorize in this sample): * searchClient.categorize.deferUpdates(true); * * @param state Turns on or off deferring of updates. * @param skipPending Used to indicate if a pending update is to be executed or skipped when deferring * is turned off. The param is ignored for `state=true`. Default is false. */ SearchClient.prototype.deferUpdates = function (state, skipPending) { if (skipPending === void 0) { skipPending = false; } this.autocomplete.deferUpdates(state, skipPending); this.categorize.deferUpdates(state, skipPending); this.find.deferUpdates(state, skipPending); }; /** * Find the category based on the category-name array. * * @param categoryName The category array that identifies the category. * @returns The Category object if found or null. */ SearchClient.prototype.findCategory = function (categoryName) { return this.categorize.findCategory(categoryName); }; /** * Gets the previous page of match-results. * Will run trigger-checks and potentially update services. */ SearchClient.prototype.matchPagePrev = function () { // Cannot fetch page less than 0 if (this._query.matchPage > 1) { this.matchPage--; return true; } return false; }; /** * Gets the next page of match-results (if any). * Will run trigger-checks and potentially update services. */ SearchClient.prototype.matchPageNext = function () { this.matchPage++; return true; }; Object.defineProperty(SearchClient.prototype, "categorizationType", { /** * Gets the currently active categorizationType value. */ get: function () { return this._query.categorizationType; }, /** * Sets the currently active categorizationType. * * Will run trigger-checks and potentially update services. */ set: function (categorizationType) { // tslint:disable-next-line:triple-equals if (categorizationType != this._query.categorizationType) { var oldValue = this._query.categorizationType; this._query.clientId = categorizationType; this.autocomplete.categorizationTypeChanged(oldValue, this._query); this.categorize.categorizationTypeChanged(oldValue, this._query); this.find.categorizationTypeChanged(oldValue, this._query); } }, enumerable: false, configurable: true }); Object.defineProperty(SearchClient.prototype, "clientId", { /** * Gets the currently active client-id value. */ get: function () { return this._query.clientId; }, /** * Sets the currently active client-id. * * Will run trigger-checks and potentially update services. */ set: function (clientId) { // tslint:disable-next-line:triple-equals if (clientId != this._query.clientId) { var oldValue = this._query.clientId; this._query.clientId = clientId; this.autocomplete.clientIdChanged(oldValue, this._query); this.categorize.clientIdChanged(oldValue, this._query); this.find.clientIdChanged(oldValue, this._query); } }, enumerable: false, configurable: true }); Object.defineProperty(SearchClient.prototype, "dateFrom", { /** * Gets the currently active date-from value. */ get: function () { return this._query.dateFrom; }, /** * Sets the from-date for matches to be used. * * Will run trigger-checks and potentially update services. */ set: function (dateFrom) { if (!deepEqual(dateFrom, this._query.dateFrom)) { var oldValue = __assign({}, this._query.dateFrom); // clone this._query.dateFrom = dateFrom; this.autocomplete.dateFromChanged(oldValue, this._query); this.categorize.dateFromChanged(oldValue, this._query); this.find.dateFromChanged(oldValue, this._query); } }, enumerable: false, configurable: true }); Object.defineProperty(SearchClient.prototype, "dateTo", { /** * Gets the currently active date-to value. */ get: function () { return this._query.dateTo; }, /** * Sets the to-date for matches to be used. * * Will run trigger-checks and potentially update services. */ set: function (dateTo) { if (!deepEqual(dateTo, this._query.dateTo)) { var oldValue = __assign({}, this._query.dateTo); // clone this._query.dateTo = dateTo; this.autocomplete.dateToChanged(oldValue, this._query); this.categorize.dateToChanged(oldValue, this._query); this.find.dateToChanged(oldValue, this._query); } }, enumerable: false, configurable: true }); Object.defineProperty(SearchClient.prototype, "filters", { /** * Gets the currently active filters. */ get: function () { return this._query.filters; }, /** * Sets the filters to be used. * * Will run trigger-checks and potentially update services. */ set: function (filters) { filters = filters || []; var sortedFilters = filters.sort(); // tslint:disable-next-line:triple-equals if (sortedFilters.join('') != this._query.filters.join('')) { var oldValue = this._query.filters.slice(0); // clone this._query.filters = sortedFilters; this.autocomplete.filtersChanged(oldValue, this._query); this.categorize.filtersChanged(oldValue, this._query); this.find.filtersChanged(oldValue, this._query); } }, enumerable: false, configurable: true }); Object.defineProperty(SearchClient.prototype, "matchGenerateContent", { /** * Gets the currently active match generateContent setting. */ get: function () { return this._query.matchGenerateContent; }, /** * Sets whether the results should generate the content or not. * * **Note:** Requires the backend IndexManager to have the option enabled in its configuration too. * * Will run trigger-checks and potentially update services. */ set: function (generateContent) { // tslint:disable-next-line:triple-equals if (generateContent != this._query.matchGenerateContent) { var oldValue = this._query.matchGenerateContent; this._query.matchGenerateContent = generateContent; this.autocomplete.matchGenerateContentChanged(oldValue, this._query); this.categorize.matchGenerateContentChanged(oldValue, this._query); this.find.matchGenerateContentChanged(oldValue, this._query); } }, enumerable: false, configurable: true }); Object.defineProperty(SearchClient.prototype, "matchGenerateContentHighlights", { /** * Gets the currently active match generateContentHighlights setting. */ get: function () { return this._query.matchGenerateContent; }, /** * Sets whether the results should generate the content-highlight tags or not. * * **Note:** See the matchGenerateContent property in regards to IndexManager requirements. * * Will run trigger-checks and potentially update services. */ set: function (generateContentHighlights) { if ( // tslint:disable-next-line:triple-equals generateContentHighlights != this._query.matchGenerateContentHighlights) { var oldValue = this._query.matchGenerateContentHighlights; this._query.matchGenerateContentHighlights = generateContentHighlights; this.autocomplete.matchGenerateContentHighlightsChanged(oldValue, this._query); this.categorize.matchGenerateContentHighlightsChanged(oldValue, this._query); this.find.matchGenerateContentHighlightsChanged(oldValue, this._query); } }, enumerable: false, configurable: true }); Object.defineProperty(SearchClient.prototype, "matchGrouping", { /** * Gets the currently active match grouping mode. */ get: function () { return this._query.matchGrouping; }, /** * Sets whether the results should be grouped or not. * * **Note:** Requires the search-service to have the option enabled in it's configuration too. * * Will run trigger-checks and potentially update services. */ set: function (useGrouping) { // tslint:disable-next-line:triple-equals if (useGrouping != this._query.matchGrouping) { var oldValue = this._query.matchGrouping; this._query.matchGrouping = useGrouping; this.autocomplete.matchGroupingChanged(oldValue, this._query); this.categorize.matchGroupingChanged(oldValue, this._query); this.find.matchGroupingChanged(oldValue, this._query); } }, enumerable: false, configurable: true }); Object.defineProperty(SearchClient.prototype, "matchPage", { /** * Gets the currently active match-page. */ get: function () { return this._query.matchPage; }, /** * Sets the match-page to get. * Will run trigger-checks and potentially update services. */ set: function (page) { if (page < 1) { throw new Error('"matchPage" cannot be set to a value smaller than 1.'); } // tslint:disable-next-line:triple-equals if (page != this._query.matchPage) { var oldValue = this._query.matchPage; this._query.matchPage = page; this.autocomplete.matchPageChanged(oldValue, this._query); this.categorize.matchPageChanged(oldValue, this._query); this.find.matchPageChanged(oldValue, this._query); } }, enumerable: false, configurable: true }); Object.defineProperty(SearchClient.prototype, "matchPageSize", { /** * Gets the currently active match page-size. */ get: function () { return this._query.matchPageSize; }, /** * Sets the match page-size to be used. * Will run trigger-checks and potentially update services. */ set: function (pageSize) { if (pageSize < 1) { throw new Error('"matchPageSize" cannot be set to a value smaller than 1.'); } // tslint:disable-next-line:triple-equals if (pageSize != this._query.matchPageSize) { var oldValue = this._query.matchPageSize; this._query.matchPageSize = pageSize; this.autocomplete.matchPageSizeChanged(oldValue, this._query); this.categorize.matchPageSizeChanged(oldValue, this._query); this.find.matchPageSizeChanged(oldValue, this._query); } }, enumerable: false, configurable: true }); Object.defineProperty(SearchClient.prototype, "matchOrderBy", { /** * Gets the currently active match order. */ get: function () { return this._query.matchOrderBy; }, /** * Sets the match sorting mode to be used. * Will run trigger-checks and potentially update services. */ set: function (orderBy) { // tslint:disable-next-line:triple-equals if (orderBy != this._query.matchOrderBy) { var oldValue = this._query.matchOrderBy; this._query.matchOrderBy = orderBy; this.autocomplete.matchOrderByChanged(oldValue, this._query); this.categorize.matchOrderByChanged(oldValue, this._query); this.find.matchOrderByChanged(oldValue, this._query); } }, enumerable: false, configurable: true }); Object.defineProperty(SearchClient.prototype, "maxSuggestions", { /** * Gets the currently active max number of autocomplete suggestions to get. */ get: function () { return this._query.maxSuggestions; }, /** * Sets the max number of autocomplete suggestions to get. * Will run trigger-checks and potentially update services. */ set: function (maxSuggestions) { if (maxSuggestions < 0) { maxSuggestions = 0; } // tslint:disable-next-line:triple-equals if (maxSuggestions != this._query.maxSuggestions) { var oldValue = this._query.maxSuggestions; this._query.maxSuggestions = maxSuggestions; this.autocomplete.maxSuggestionsChanged(oldValue, this._query); this.categorize.maxSuggestionsChanged(oldValue, this._query); this.find.maxSuggestionsChanged(oldValue, this._query); } }, enumerable: false, configurable: true }); Object.defineProperty(SearchClient.prototype, "query", { /** * Returns the currently active query. */ get: function () { return this._query; }, /** * Sets the query to use. Consider using the queryText-property for query-text-changes instead. * * **Note:** Changing the `query` property will likely lead to multiple trigger-checks and potential updates. * This is because changing the whole value will lead to each of the query-objects' properties to trigger individual * events. * * To avoid multiple updates, call `deferUpdates(true)` before and deferUpdates(false) afterwards. Then at max * only one update will be generated. */ set: function (query) { this.clientId = query.clientId; this.dateFrom = query.dateFrom; this.dateTo = query.dateTo; this.filters = query.filters; this.matchGrouping = query.matchGrouping; this.matchOrderBy = query.matchOrderBy; this.matchPage = query.matchPage; this.matchPageSize = query.matchPageSize; this.maxSuggestions = query.maxSuggestions; this.queryText = query.queryText; this.searchType = query.searchType; }, enumerable: false, configurable: true }); Object.defineProperty(SearchClient.prototype, "queryText", { /** * Gets the currently active query-object. */ get: function () { return this._query.queryText; }, /** * Sets the query-text to be used. * Will run trigger-checks and potentially update services. */ set: function (queryText) { // tslint:disable-next-line:triple-equals if (queryText != this._query.queryText) { var oldValue = this._query.queryText; this._query.queryText = queryText; this.autocomplete.queryTextChanged(oldValue, this._query); this.categorize.queryTextChanged(oldValue, this._query); this.find.queryTextChanged(oldValue, this._query); } }, enumerable: false, configurable: true }); Object.defineProperty(SearchClient.prototype, "searchType", { /** * Gets the currently active search-type value. */ get: function () { return this._query.searchType; }, /** * Sets the search-type to be used. * Will run trigger-checks and potentially update services. */ set: function (searchType) { // tslint:disable-next-line:triple-equals if (searchType != this._query.searchType) { var oldValue = this._query.searchType; this._query.searchType = searchType; this.autocomplete.searchTypeChanged(oldValue, this._query); this.categorize.searchTypeChanged(oldValue, this._query); this.find.searchTypeChanged(oldValue, this._query); } }, enumerable: false, configurable: true }); Object.defineProperty(SearchClient.prototype, "uiLanguageCode", { /** * Gets the currently active match generateContent setting. */ get: function () { return this._query.uiLanguageCode; }, /** * Sets the language that the client uses. Affects category-names (and in the future maybe metadata too). * The expected values should be according to the https://www.wikiwand.com/en/IETF_language_tag standard. * * Changes will run trigger-checks and potentially update services. */ set: function (uiLanguageCode) { // tslint:disable-next-line:triple-equals if (uiLanguageCode != this._query.uiLanguageCode) { var oldValue = this._query.uiLanguageCode; this._query.uiLanguageCode = uiLanguageCode; this.autocomplete.uiLanguageCodeChanged(oldValue, this._query); this.categorize.uiLanguageCodeChanged(oldValue, this._query); this.find.uiLanguageCodeChanged(oldValue, this._query); } }, enumerable: false, configurable: true }); SearchClient.prototype.doFilterAdd = function (filter) { var _this = this; // Find item in categorize.categories, and build displayName for the Filter (displayName for each categoryNode in the hierarchy) var newFilter = this.categorize.createCategoryFilter(filter); if (!newFilter) { return; } var oldValue = this._query.filters.slice(0); var toRemove = []; // Find parent filters on the same path (to be removed) var filterName = newFilter.category.categoryName[0]; for (var i = 1; i < newFilter.category.categoryName.length - 1; i++) { filterName += "|" + newFilter.category.categoryName[i]; this._query.filters.forEach(function (f) { var fName = f.category.categoryName.join('|'); if (fName === filterName) { toRemove.push(f); } }); } filterName += "|" + newFilter.category.categoryName[newFilter.category.categoryName.length - 1]; // Find child filters of the same path (to be removed) this._query.filters.forEach(function (f) { var fName = f.category.categoryName.join('|'); if (fName.startsWith(filterName)) { toRemove.push(f); } }); // Execute the actual remove (without triggering an update). toRemove.forEach(function (f) { _this._query.filters.forEach(function (item, index) { if (item === f) { _this._query.filters.splice(index, 1); } }); }); // Add the new filter this._query.filters.push(newFilter); this._query.filters.sort(); this.autocomplete.filtersChanged(oldValue, this._query); this.categorize.filtersChanged(oldValue, this._query); this.find.filtersChanged(oldValue, this._query); }; SearchClient.prototype.doFilterRemove = function (i) { var oldValue = this._query.filters.slice(0); this._query.filters.splice(i, 1); // Note: No need to sort the filter-list afterwards, as removing an item cannot change the order anyway. this.autocomplete.filtersChanged(oldValue, this._query); this.categorize.filtersChanged(oldValue, this._query); this.find.filtersChanged(oldValue, this._query); return true; }; SearchClient.prototype.setup = function (settings, fetchMethod) { // Make sure that we keep the original settings and fetch-method, for the reset-function to reuse later. this._origSettings = clone(settings); this._origFetchMethod = clone(fetchMethod); this.settings = new Settings(settings); this.authentication = new AuthenticationFactory().create(this.settings.authentication, this, fetchMethod); this.settings.authentication = this.authentication.settings; this.autocomplete = new Autocomplete(this.settings.autocomplete, this, fetchMethod); this.settings.autocomplete = this.autocomplete.settings; this.categorize = new Categorize(this.settings.categorize, this, fetchMethod); this.settings.categorize = this.categorize.settings; this.find = new Find(this.settings.find, this, fetchMethod); this.settings.find = this.find.settings; this._query = new Query(this.settings.query); }; return SearchClient; }()); export { SearchClient }; //# sourceMappingURL=SearchClient.js.map