diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 000000000..a86d76d03 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["semistandard"], + "ignorePatterns": ["dist", "docs"], + "globals": { + "beforeEach": true, + "beforeAll": true, + "describe": true, + "it": true, + "expect": true, + "jest": true, + "DOMParser": true, + "ytag": true, + "test": true, + "fixture": true, + "CustomEvent": true, + "ANSWERS": true + } +} diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 000000000..64b30f02c --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,31 @@ +# This workflow will run our tests, generate an lcov code coverage file, +# and send that coverage to Coveralls + +name: Code Coverage + +on: + push: + branches-ignore: dev/* + pull_request: + +jobs: + Coveralls: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [14.x] + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm ci + - run: npx gulp templates + - run: npx jest --coverage + - name: Coveralls + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/npm_publish.yml b/.github/workflows/npm_publish.yml new file mode 100644 index 000000000..2f5bc5a7d --- /dev/null +++ b/.github/workflows/npm_publish.yml @@ -0,0 +1,31 @@ +name: NPM publish on new release + +on: + release: + types: [created] + branches: + - master + +jobs: + npm-publish: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [15.x] + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + registry-url: https://registry.npmjs.org/ + - run: npm ci + - run: npm run build + - run: npm run prepublishOnly + - run: npm publish + - run: npm run postpublish + env: + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} diff --git a/.npmignore b/.npmignore deleted file mode 100644 index ef5ccfd3b..000000000 --- a/.npmignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules/ -**/.DS_Store diff --git a/README.md b/README.md index f7b3efddd..37065b45e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # Answers Search UI -Outline: + +

+ + Coverage Status + +

+ 1. [Install / Setup](#install-and-setup) 2. [ANSWERS.init Configuration Options](#answersinit-configuration-options) - [Vertical Pages Configuration](#vertical-pages-configuration) diff --git a/conf/gulp-tasks/library.gulpfile.js b/conf/gulp-tasks/library.gulpfile.js index 66cc2f225..ceddd7d99 100644 --- a/conf/gulp-tasks/library.gulpfile.js +++ b/conf/gulp-tasks/library.gulpfile.js @@ -2,6 +2,7 @@ const { series, parallel, src, dest, watch } = require('gulp'); const path = require('path'); const postcss = require('gulp-postcss'); const sass = require('gulp-sass'); +sass.compiler = require('sass'); const getLibraryVersion = require('./utils/libversion'); const { BundleType, BundleTaskFactory } = require('./bundle/bundletaskfactory'); diff --git a/conf/gulp-tasks/template/bundletemplatestaskfactory.js b/conf/gulp-tasks/template/bundletemplatestaskfactory.js index 5f15cd54a..f9401caac 100644 --- a/conf/gulp-tasks/template/bundletemplatestaskfactory.js +++ b/conf/gulp-tasks/template/bundletemplatestaskfactory.js @@ -111,7 +111,7 @@ class BundleTemplatesTaskFactory { plugins: [ '@babel/syntax-dynamic-import', ['@babel/plugin-transform-runtime', { - 'corejs': 3 + corejs: 3 }], '@babel/plugin-transform-arrow-functions', '@babel/plugin-proposal-object-rest-spread', diff --git a/conf/gulp-tasks/template/createcleanfiles.js b/conf/gulp-tasks/template/createcleanfiles.js index a4f35cef4..97cafb467 100644 --- a/conf/gulp-tasks/template/createcleanfiles.js +++ b/conf/gulp-tasks/template/createcleanfiles.js @@ -27,7 +27,7 @@ function createCleanFilesTask (locale) { */ function _cleanFiles (callback, locale) { const precompiledFile = getPrecompiledFileName(locale); - del.sync([ `./dist/${precompiledFile}` ]); + del.sync([`./dist/${precompiledFile}`]); callback(); } diff --git a/conf/gulp-tasks/template/createprecompiletemplates.js b/conf/gulp-tasks/template/createprecompiletemplates.js index e85bb4a98..5e24bada5 100644 --- a/conf/gulp-tasks/template/createprecompiletemplates.js +++ b/conf/gulp-tasks/template/createprecompiletemplates.js @@ -54,7 +54,7 @@ function precompileTemplates (callback, outputFile, processAST) { processPartialName: function (fileName, a, b, c) { // Strip the extension and the underscore // Escape the output with JSON.stringify - let name = fileName.split('.')[0]; + const name = fileName.split('.')[0]; if (name.charAt(0) === '_') { return JSON.stringify(name.substr(1)); } else { @@ -64,8 +64,8 @@ function precompileTemplates (callback, outputFile, processAST) { // TBH, this isn't really needed anymore since we don't name files like so 'foo.bar.js', but this is here to // support that use case. customContext: function (fileName) { - let name = fileName.split('.')[0]; - let keys = name.split('.'); + const name = fileName.split('.')[0]; + const keys = name.split('.'); let context = 'context'; for (let i = 0; i < keys.length; i++) { context = context += '["' + keys[i] + '"]'; @@ -78,7 +78,7 @@ function precompileTemplates (callback, outputFile, processAST) { root: 'context', noRedeclare: true, processName: function (filePath) { - let path = filePath.replace('src/ui/templates', ''); + const path = filePath.replace('src/ui/templates', ''); return declare.processNameByPath(path, '').replace('.', '/'); } })) diff --git a/conf/i18n/constants.js b/conf/i18n/constants.js index a753acb0e..1e9704b1b 100644 --- a/conf/i18n/constants.js +++ b/conf/i18n/constants.js @@ -3,7 +3,7 @@ exports.TRANSLATION_FLAGGER_REGEX = /TranslationFlagger.flag\((\s)*\{[^;]+?\}(\s exports.DEFAULT_LOCALE = 'en'; const LANGUAGES_TO_LOCALES = { - 'en': [ + en: [ 'en_AE', 'en_AI', 'en_AS', @@ -65,7 +65,7 @@ const LANGUAGES_TO_LOCALES = { 'en_US', 'en_ZA' ], - 'es': [ + es: [ 'es_BO', 'es_CO', 'es_CU', @@ -77,7 +77,7 @@ const LANGUAGES_TO_LOCALES = { 'es_NI', 'es_US' ], - 'fr': [ + fr: [ 'fr_BE', 'fr_BL', 'fr_CA', @@ -89,18 +89,18 @@ const LANGUAGES_TO_LOCALES = { 'fr_MC', 'fr_RE' ], - 'it': [ + it: [ 'it_CH', 'it_IT' ], - 'de': [ + de: [ 'de_AT', 'de_CH', 'de_DE', 'de_EU', 'de_LU' ], - 'ja': [ + ja: [ 'ja_JP' ] }; diff --git a/conf/i18n/runtimecallgeneratorutils.js b/conf/i18n/runtimecallgeneratorutils.js index c62fadc7b..6bf00e1d4 100644 --- a/conf/i18n/runtimecallgeneratorutils.js +++ b/conf/i18n/runtimecallgeneratorutils.js @@ -52,6 +52,6 @@ function formatPluralForms (translatorResult) { * @returns {string} */ function escapeSingleQuotes (str) { - const regex = new RegExp('\'', 'g'); + const regex = /'/g; return str.replace(regex, '\\\''); } diff --git a/conf/i18n/translatehelpervisitor.js b/conf/i18n/translatehelpervisitor.js index 0215cf8af..7c3b12e59 100644 --- a/conf/i18n/translatehelpervisitor.js +++ b/conf/i18n/translatehelpervisitor.js @@ -107,7 +107,7 @@ class TranslateHelperVisitor { ...statement, path: { ...statement.path, - parts: [ this._processTranslationHelper ], + parts: [this._processTranslationHelper], original: this._processTranslationHelper } }; diff --git a/conf/i18n/translator.js b/conf/i18n/translator.js index 67f3b3680..ed5603821 100644 --- a/conf/i18n/translator.js +++ b/conf/i18n/translator.js @@ -164,7 +164,7 @@ class Translator { const placeholders = {}; let placeholderMatch; - const placeholderRegex = new RegExp(/\[\[([a-zA-Z0-9]+)\]\]/, 'g'); + const placeholderRegex = /\[\[([a-zA-Z0-9]+)\]\]/g; while ((placeholderMatch = placeholderRegex.exec(phrase)) != null) { if (placeholderMatch.length >= 2) { placeholders[placeholderMatch[1]] = placeholderMatch[0]; diff --git a/docs/RichTextFormatterImpl.html b/docs/RichTextFormatterImpl.html new file mode 100644 index 000000000..4f2c0295c --- /dev/null +++ b/docs/RichTextFormatterImpl.html @@ -0,0 +1,588 @@ + + + + + JSDoc: Class: RichTextFormatterImpl + + + + + + + + + + +
+ +

Class: RichTextFormatterImpl

+ + + + + + +
+ +
+ +

RichTextFormatterImpl()

+ +
This class leverages the RtfConverter library to perform Rich Text to +HTML conversions.
+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new RichTextFormatterImpl()

+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +

Methods

+ + + + + + + +

_generatePluginName() → {string}

+ + + + + + +
+ A function that generates a unique UUID to serve as the name for a +Markdown-it plugin. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ the UUID. +
+ + + +
+
+ Type +
+
+ +string + + +
+
+ + + + + + + + + + + + + +

_urlTransformer()

+ + + + + + +
+ An inline token parser for use with the iterator Markdown-it plugin. +This token parser adds a cta-type data attribute to any link it encounters. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

format(fieldValue, fieldName, targetConfig) → {string}

+ + + + + + +
+ Generates an HTML representation of the provided Rich Text field value. Note that +the HTML will contain a wrapper div. This is to support click analytics for Rich Text +links. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
fieldValue + + +string + + + + A Rich Text field value.
fieldName + + +string + + + + The name of the field, to be included in the payload of a click + analytics event. This parameter is optional.
targetConfig + + +Object +| + +string + + + + Configuration object specifying the 'target' behavior for + the various types of links. If a string is provided, it is assumed that + is the 'target' behavior across all types of links. This parameter is optional.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ The HTML representation of the field value, serialized as a string. +
+ + + +
+
+ Type +
+
+ +string + + +
+
+ + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/docs/answers-umd.js.html b/docs/answers-umd.js.html index 37d1113d7..eb7e38312 100644 --- a/docs/answers-umd.js.html +++ b/docs/answers-umd.js.html @@ -29,16 +29,49 @@

Source: answers-umd.js

/** @module */
 
 import Core from './core/core';
+import cssVars from 'css-vars-ponyfill';
 
 import {
-  TemplateLoader,
-  COMPONENT_MANAGER,
+  DefaultTemplatesLoader,
   Renderers,
-  DOM
+  DOM,
+  SearchParams
 } from './ui/index';
+import Component from './ui/components/component';
 
 import ErrorReporter from './core/errors/errorreporter';
-import { AnalyticsReporter } from './core';
+import ConsoleErrorReporter from './core/errors/consoleerrorreporter';
+import { AnalyticsReporter, NoopAnalyticsReporter } from './core';
+import Storage from './core/storage/storage';
+import { AnswersComponentError } from './core/errors/errors';
+import AnalyticsEvent from './core/analytics/analyticsevent';
+import StorageKeys from './core/storage/storagekeys';
+import QueryTriggers from './core/models/querytriggers';
+import SearchConfig from './core/models/searchconfig';
+import ComponentManager from './ui/components/componentmanager';
+import VerticalPagesConfig from './core/models/verticalpagesconfig';
+import { SANDBOX, PRODUCTION, LOCALE, QUERY_SOURCE } from './core/constants';
+import RichTextFormatter from './core/utils/richtextformatter';
+import { isValidContext } from './core/utils/apicontext';
+import FilterNodeFactory from './core/filters/filternodefactory';
+import { urlWithoutQueryParamsAndHash } from './core/utils/urlutils';
+import TranslationProcessor from './core/i18n/translationprocessor';
+import Filter from './core/models/filter';
+import SearchComponent from './ui/components/search/searchcomponent';
+import QueryUpdateListener from './core/statelisteners/queryupdatelistener';
+
+/** @typedef {import('./core/services/errorreporterservice').default} ErrorReporterService */
+/** @typedef {import('./core/services/analyticsreporterservice').default} AnalyticsReporterService */
+
+/**
+ * @typedef Services
+ * @property {ErrorReporterService} errorReporterService
+ */
+
+const DEFAULTS = {
+  locale: LOCALE,
+  querySource: QUERY_SOURCE
+};
 
 /**
  * The main Answers interface
@@ -49,6 +82,23 @@ 

Source: answers-umd.js

return Answers.getInstance(); } + /** + * A reference to the Component base class for custom + * components to extend + */ + this.Component = Component; + + /** + * A reference to the AnalyticsEvent base class for reporting + * custom analytics + */ + this.AnalyticsEvent = AnalyticsEvent; + + /** + * A reference to the FilterNodeFactory class for creating {@link FilterNode}s. + */ + this.FilterNodeFactory = FilterNodeFactory; + /** * A reference of the renderer to use for the components * This is provided during initialization. @@ -56,17 +106,48 @@

Source: answers-umd.js

*/ this.renderer = new Renderers.Handlebars(); + /** + * A reference to the formatRichText function. + * @type {Function} + */ + this.formatRichText = (markdown, eventOptionsFieldName, targetConfig) => + RichTextFormatter.format(markdown, eventOptionsFieldName, targetConfig); + /** * A local reference to the component manager * @type {ComponentManager} */ - this.components = COMPONENT_MANAGER; + this.components = ComponentManager.getInstance(); + + /** + * A local reference to the core api + * @type {Core} + */ + this.core = null; /** * A callback function to invoke once the library is ready. * Typically fired after templates are fetched from server for rendering. */ this._onReady = function () {}; + + /** + * @type {boolean} + * @private + */ + this._eligibleForAnalytics = false; + + /** + * @type {Services} + * @private + */ + this._services = null; + + /** + * @type {AnalyticsReporterService} + * @private + */ + this._analyticsReporterService = null; } static setInstance (instance) { @@ -81,43 +162,228 @@

Source: answers-umd.js

return this.instance; } - init (config) { - this.components.setCore(new Core({ - apiKey: config.apiKey, - answersKey: config.answersKey, - locale: config.locale - })) - .setRenderer(this.renderer) - .setAnalyticsReporter(new AnalyticsReporter(config.apiKey, config.answersKey)); + _initStorage (parsedConfig) { + const storage = new Storage({ + updateListener: (data, url) => { + if (parsedConfig.onStateChange) { + parsedConfig.onStateChange(Object.fromEntries(data), url); + } + }, + resetListener: data => { + let query = data.get(StorageKeys.QUERY); + const hasQuery = query || query === ''; + this.core.storage.delete(StorageKeys.PERSISTED_LOCATION_RADIUS); + this.core.storage.delete(StorageKeys.PERSISTED_FILTER); + this.core.storage.delete(StorageKeys.PERSISTED_FACETS); + this.core.storage.delete(StorageKeys.SORT_BYS); + this.core.filterRegistry.clearAllFilterNodes(); + + if (!hasQuery) { + this.core.clearResults(); + } else { + this.core.storage.set(StorageKeys.QUERY_TRIGGER, QueryTriggers.QUERY_PARAMETER); + } + + if (!data.get(StorageKeys.SEARCH_OFFSET)) { + this.core.storage.set(StorageKeys.SEARCH_OFFSET, 0); + } + + data.forEach((value, key) => { + if (key === StorageKeys.QUERY) { + return; + } + const parsedValue = this._parsePersistentStorageValue(key, value); + this.core.storage.set(key, parsedValue); + }); + + this.core.storage.set(StorageKeys.HISTORY_POP_STATE, data); + + if (hasQuery) { + this.core.storage.set(StorageKeys.QUERY, query); + } + }, + persistedValueParser: this._parsePersistentStorageValue + }); + storage.init(window.location.search); + storage.set(StorageKeys.SEARCH_CONFIG, parsedConfig.search); + storage.set(StorageKeys.VERTICAL_PAGES_CONFIG, parsedConfig.verticalPages); + storage.set(StorageKeys.LOCALE, parsedConfig.locale); + storage.set(StorageKeys.QUERY_SOURCE, parsedConfig.querySource); + + // Check if sessionsOptIn data is stored in the URL. If it is, prefer that over + // what is in parsedConfig. + const sessionOptIn = storage.get(StorageKeys.SESSIONS_OPT_IN); + if (!sessionOptIn) { + storage.set( + StorageKeys.SESSIONS_OPT_IN, + { value: parsedConfig.sessionTrackingEnabled, setDynamically: false }); + } else { + // If sessionsOptIn was stored in the URL, it was stored only as a string. + // Parse this value and add it back to storage. + storage.set( + StorageKeys.SESSIONS_OPT_IN, + { value: (/^true$/i).test(sessionOptIn), setDynamically: true }); + } - this._onReady = config.onReady || function () {}; + parsedConfig.noResults && storage.set(StorageKeys.NO_RESULTS_CONFIG, parsedConfig.noResults); + const isSuggestQueryTrigger = + storage.get(StorageKeys.QUERY_TRIGGER) === QueryTriggers.SUGGEST; + if (storage.has(StorageKeys.QUERY) && !isSuggestQueryTrigger) { + storage.set(StorageKeys.QUERY_TRIGGER, QueryTriggers.QUERY_PARAMETER); + } - if (config.useTemplates === false || config.templateBundle) { - if (config.templateBundle) { - this.renderer.init(config.templateBundle); - } + const context = storage.get(StorageKeys.API_CONTEXT); + if (context && !isValidContext(context)) { + storage.delete(StorageKeys.API_CONTEXT); + console.error(`Context parameter "${context}" is invalid, omitting from the search.`); + } - this._onReady(); - return this; + if (storage.get(StorageKeys.REFERRER_PAGE_URL) === undefined) { + storage.set( + StorageKeys.REFERRER_PAGE_URL, + urlWithoutQueryParamsAndHash(document.referrer) + ); + } + return storage; + } + + /** + * Initializes the SDK with the provided configuration. Note that before onReady + * is ever called, a check to the relevant Answers Status page is made. + * + * @param {Object} config The Answers configuration. + * @param {Object} statusPage An override for the baseUrl and endpoint of the + * experience's Answers Status page. + */ + init (config, statusPage) { + window.performance.mark('yext.answers.initStart'); + const parsedConfig = this.parseConfig(config); + this.validateConfig(parsedConfig); + + parsedConfig.search = new SearchConfig(parsedConfig.search); + parsedConfig.verticalPages = new VerticalPagesConfig(parsedConfig.verticalPages); + + const storage = this._initStorage(parsedConfig); + this._services = parsedConfig.mock + ? getMockServices() + : getServices(parsedConfig, storage); + + this._eligibleForAnalytics = parsedConfig.businessId != null; + // TODO(amullings): Initialize with other services + if (this._eligibleForAnalytics && parsedConfig.mock) { + this._analyticsReporterService = new NoopAnalyticsReporter(); + } else if (this._eligibleForAnalytics) { + this._analyticsReporterService = new AnalyticsReporter( + parsedConfig.experienceKey, + parsedConfig.experienceVersion, + parsedConfig.businessId, + parsedConfig.analyticsOptions, + parsedConfig.environment); + + // listen to query id updates + storage.registerListener({ + eventType: 'update', + storageKey: StorageKeys.QUERY_ID, + callback: id => this._analyticsReporterService.setQueryId(id) + }); + + this.components.setAnalyticsReporter(this._analyticsReporterService); + initScrollListener(this._analyticsReporterService); + } + + this.core = new Core({ + apiKey: parsedConfig.apiKey, + storage: storage, + experienceKey: parsedConfig.experienceKey, + fieldFormatters: parsedConfig.fieldFormatters, + experienceVersion: parsedConfig.experienceVersion, + locale: parsedConfig.locale, + analyticsReporter: this._analyticsReporterService, + onVerticalSearch: parsedConfig.onVerticalSearch, + onUniversalSearch: parsedConfig.onUniversalSearch, + environment: parsedConfig.environment, + componentManager: this.components + }); + + if (parsedConfig.onStateChange && typeof parsedConfig.onStateChange === 'function') { + parsedConfig.onStateChange( + Object.fromEntries(storage.getAll()), + this.core.storage.getCurrentStateUrlMerged()); } - // Templates are currently downloaded separately from the CORE and UI bundle. - // Future enhancement is to ship the components with templates in a separate bundle. - this.templates = new TemplateLoader({ - templateUrl: config.templateUrl - }).onLoaded((templates) => { - this.renderer.init(templates); + this.components + .setCore(this.core) + .setRenderer(this.renderer); + + this._setDefaultInitialSearch(parsedConfig.search); + + this.core.init(); + + this._onReady = parsedConfig.onReady || function () {}; + const asyncDeps = this._loadAsyncDependencies(parsedConfig); + return asyncDeps.finally(() => { this._onReady(); + if (!this.components.getActiveComponent(SearchComponent.type)) { + this._initQueryUpdateListener(parsedConfig.search); + } + this._searchOnLoad(); + }); + } + + _initQueryUpdateListener ({ verticalKey, defaultInitialSearch }) { + const queryUpdateListener = new QueryUpdateListener(this.core, { + defaultInitialSearch, + verticalKey }); + this.core.setQueryUpdateListener(queryUpdateListener); + } - if (!config.suppressErrorReports) { - this._errorReporter = new ErrorReporter(config.apiKey, config.answersKey); - window.addEventListener('error', e => this._errorReporter.report(e.error)); - window.addEventListener('unhandledrejection', e => this._errorReporter.report(e.error)); + /** + * This guarantees that execution of the SearchBar's search on page load occurs only + * AFTER all components have been added to the page. Trying to do this with a regular + * onCreate relies on the SearchBar having some sort of async behavior to move the execution + * of the search to the end of the call stack. For instance, relying on promptForLocation + * being set to true, which adds additional Promises that will delay the exeuction. + * + * We need to guarantee that the searchOnLoad happens after the onReady, because certain + * components will update values in storage in their onMount/onCreate, which are then expected + * to be applied to this search on page load. For example, filter components can apply + * filters on page load, which must be applied before this search is made to affect it. + * + * If no special search components exist, we still want to search on load if a query has been set, + * either from a defaultInitialSearch or from a query in the URL. + */ + _searchOnLoad () { + const searchComponents = this.components._activeComponents + .filter(c => c.constructor.type === SearchComponent.type); + if (searchComponents.length) { + searchComponents.forEach(c => c.searchAfterAnswersOnReady && c.searchAfterAnswersOnReady()); + } else if (this.core.storage.has(StorageKeys.QUERY)) { + this.core.triggerSearch(this.core.storage.get(StorageKeys.QUERY_TRIGGER)); } + } - return this; + _loadAsyncDependencies (parsedConfig) { + const loadTemplates = this._loadTemplates(parsedConfig); + const ponyfillCssVariables = this._handlePonyfillCssVariables(parsedConfig.disableCssVariablesPonyfill); + return Promise.all([loadTemplates, ponyfillCssVariables]); + } + + _loadTemplates ({ useTemplates, templateBundle }) { + if (useTemplates === false || templateBundle) { + if (templateBundle) { + this.renderer.init(templateBundle, this._getInitLocale()); + return Promise.resolve(); + } + } else { + // Templates are currently downloaded separately from the CORE and UI bundle. + // Future enhancement is to ship the components with templates in a separate bundle. + this.templates = new DefaultTemplatesLoader(templates => { + this.renderer.init(templates, this._getInitLocale()); + }); + return this.templates.fetchTemplates(); + } } domReady (cb) { @@ -129,6 +395,62 @@

Source: answers-umd.js

return this; } + /** + * Parses the config provided by the user. In the parsed config, any options not supplied by the + * user are given default values. + * @param {Object} config The user supplied config. + */ + parseConfig (config) { + const parsedConfig = Object.assign({}, DEFAULTS, config); + let sessionTrackingEnabled = true; + if (typeof config.sessionTrackingEnabled === 'boolean') { + sessionTrackingEnabled = config.sessionTrackingEnabled; + } + parsedConfig.sessionTrackingEnabled = sessionTrackingEnabled; + + const sandboxPrefix = `${SANDBOX}-`; + parsedConfig.apiKey.includes(sandboxPrefix) + ? parsedConfig.environment = SANDBOX + : parsedConfig.environment = PRODUCTION; + parsedConfig.apiKey = parsedConfig.apiKey.replace(sandboxPrefix, ''); + + return parsedConfig; + } + + /** + * Validates the Answers config object to ensure things like api key and experience key are + * properly set. + * @param {Object} config The Answers config. + */ + validateConfig (config) { + // TODO (tmeyer): Extract this method into it's own class. Investigate the use of JSON schema + // to validate these configs. + if (typeof config.apiKey !== 'string') { + throw new Error('Missing required `apiKey`. Type must be {string}'); + } + + if (typeof config.experienceKey !== 'string') { + throw new Error('Missing required `experienceKey`. Type must be {string}'); + } + + if (config.onVerticalSearch && typeof config.onVerticalSearch !== 'function') { + throw new Error('onVerticalSearch must be a function. Current type is: ' + typeof config.onVerticalSearch); + } + + if (config.onUniversalSearch && typeof config.onUniversalSearch !== 'function') { + throw new Error('onUniversalSearch must be a function. Current type is: ' + typeof config.onUniversalSearch); + } + } + + /** + * Register a custom component type so it can be created via + * addComponent and used as a child component + * @param {Component} componentClass + */ + registerComponentType (componentClass) { + this.components.register(componentClass); + } + addComponent (type, opts) { if (typeof opts === 'string') { opts = { @@ -136,18 +458,275 @@

Source: answers-umd.js

}; } - this.components.create(type, opts).mount(); + try { + this.components.create(type, opts).mount(); + } catch (e) { + throw new AnswersComponentError('Failed to add component', type, e); + } return this; } + /** + * Remove the component - and all of its children - with the given name + * @param {string} name The name of the component to remove + */ + removeComponent (name) { + this.components.removeByName(name); + } + createComponent (opts) { return this.components.create('Component', opts).mount(); } + /** + * Conducts a search in the Answers experience + * + * @param {string} query + */ + search (query) { + this.core.storage.setWithPersist(StorageKeys.QUERY, query); + } + registerHelper (name, cb) { this.renderer.registerHelper(name, cb); return this; } + + /** + * Compile and add a template to the current renderer + * @param {string} templateName The unique name for the template + * @param {string} template The handlebars template string + */ + registerTemplate (templateName, template) { + this.renderer.registerTemplate(templateName, template); + } + + /** + * Opt in or out of convertion tracking analytics + * @param {boolean} optIn + */ + setConversionsOptIn (optIn) { + if (this._eligibleForAnalytics) { + this._analyticsReporterService.setConversionTrackingEnabled(optIn); + } + } + + /** + * Opt in or out of session cookies + * @param {boolean} optIn + */ + setSessionsOptIn (optIn) { + this.core.storage.set( + StorageKeys.SESSIONS_OPT_IN, { value: optIn, setDynamically: true }); + } + + /** + * Sets a search query on initialization for vertical searchers that have a + * defaultInitialSearch provided, if the user hasn't already provided their + * own via URL param. A default initial search should not be persisted in the URL, + * so we do a regular set instead of a setWithPersist here. + * + * @param {SearchConfig} searchConfig + * @private + */ + _setDefaultInitialSearch (searchConfig) { + if (searchConfig.defaultInitialSearch == null) { + return; + } + const prepopulatedQuery = this.core.storage.get(StorageKeys.QUERY); + if (prepopulatedQuery != null) { + return; + } + this.core.storage.set(StorageKeys.QUERY_TRIGGER, QueryTriggers.INITIALIZE); + this.core.storage.set(StorageKeys.QUERY, searchConfig.defaultInitialSearch); + } + + /** + * Sets the geolocation tag in storage, overriding other inputs. Do not use in conjunction + * with other components that will set the geolocation internally. + * @param {number} lat + * @param {number} long + */ + setGeolocation (lat, lng) { + this.core.storage.set(StorageKeys.GEOLOCATION, { + lat, lng, radius: 0 + }); + } + + /** + * A promise that resolves when ponyfillCssVariables resolves, + * or resolves immediately if ponyfill is disabled + * @param {boolean} option to opt out of the css variables ponyfill + * @return {Promise} resolves after ponyfillCssVariables, or immediately if disabled + */ + _handlePonyfillCssVariables (ponyfillDisabled) { + window.performance.mark('yext.answers.ponyfillStart'); + if (ponyfillDisabled) { + window.performance.mark('yext.answers.ponyfillEnd'); + return Promise.resolve(); + } + return new Promise((resolve, reject) => { + this.ponyfillCssVariables({ + onFinally: () => { + window.performance.mark('yext.answers.ponyfillEnd'); + resolve(); + } + }); + }); + } + + /* + * Updates the css styles with new current variables. This is useful when the css + * variables are updated dynamically (e.g. through js) or if the css variables are + * added after the ANSWERS.init + * + * To solve issues with non-zero max-age cache controls for link/script assets in IE11, + * we add a cache busting parameter so that XMLHttpRequests succeed. + * + * @param {Object} config Additional config to pass to the ponyfill + */ + ponyfillCssVariables (config = {}) { + cssVars({ + onlyLegacy: true, + onError: config.onError || function () {}, + onSuccess: config.onSuccess || function () {}, + onFinally: config.onFinally || function () {}, + onBeforeSend: (xhr, node, url) => { + try { + const uriWithCacheBust = new URL(url); + const params = new SearchParams(uriWithCacheBust.search); + params.set('_', new Date().getTime()); + uriWithCacheBust.search = params.toString(); + xhr.open('GET', uriWithCacheBust.toString()); + } catch (e) { + // Catch the error and continue if the URL provided in the asset is not a valid URL + } + } + }); + } + + /* + * Adds context as a parameter for the query API calls. + * @param {Object} context The context object passed in the API calls + */ + setContext (context) { + const contextString = JSON.stringify(context); + if (!isValidContext(contextString)) { + console.error(`Context parameter "${context}" is invalid, omitting from the search.`); + return; + } + + this.core.storage.set(StorageKeys.API_CONTEXT, contextString); + } + + /** + * Processes a translation which includes performing interpolation, pluralization, or + * both + * @param {string | Object} translations The translation, or an object containing + * translated plural forms + * @param {Object} interpolationParams Params to use during interpolation + * @param {number} count The count associated with the pluralization + * @param {string} language The langauge associated with the pluralization + * @returns {string} The translation with any interpolation or pluralization applied + */ + processTranslation (translations, interpolationParams, count, language) { + const initLocale = this._getInitLocale(); + language = language || initLocale.substring(0, 2); + + if (!this.renderer) { + console.error('The renderer must be initialized before translations can be processed'); + return ''; + } + + const escapeExpression = this.renderer.escapeExpression.bind(this.renderer); + + return TranslationProcessor.process(translations, interpolationParams, count, language, escapeExpression); + } + + /** + * Gets the locale that ANSWERS was initialized to + * + * @returns {string} + */ + _getInitLocale () { + return this.core.storage.get(StorageKeys.LOCALE); + } + + /** + * Parses a value from persistent storage, which stores strings, + * into the shape the SDK expects. + * TODO(SLAP-1111): Move this into a dedicated file/class. + * + * @param {string} key + * @param {string} value + * @returns {string|number|Filter} + */ + _parsePersistentStorageValue (key, value) { + switch (key) { + case StorageKeys.PERSISTED_FILTER: + return Filter.from(JSON.parse(value)); + case StorageKeys.PERSISTED_LOCATION_RADIUS: + return parseFloat(value); + case StorageKeys.PERSISTED_FACETS: + case StorageKeys.SORT_BYS: + return JSON.parse(value); + default: + return value; + } + } +} + +/** + * @param {Object} config + * @param {Storage} storage + * @returns {Services} + */ +function getServices (config, storage) { + return { + errorReporterService: new ErrorReporter( + { + apiKey: config.apiKey, + experienceKey: config.experienceKey, + experienceVersion: config.experienceVersion, + printVerbose: config.debug, + sendToServer: !config.suppressErrorReports, + environment: config.environment + }, + storage) + }; +} + +/** + * @returns {Services} + */ +function getMockServices () { + return { + errorReporterService: new ConsoleErrorReporter() + }; +} + +/** + * Initialize the scroll event listener to send analytics events + * when the user scrolls to the bottom. Debounces scroll events so + * they are processed after the user stops scrolling + */ +function initScrollListener (reporter) { + const DEBOUNCE_TIME = 100; + let timeout = null; + + const sendEvent = () => { + if ((window.innerHeight + window.pageYOffset) >= document.body.scrollHeight) { + const event = new AnalyticsEvent('SCROLL_TO_BOTTOM_OF_PAGE'); + if (reporter.getQueryId()) { + reporter.report(event); + } + } + }; + + document.addEventListener('scroll', () => { + clearTimeout(timeout); + timeout = setTimeout(sendEvent, DEBOUNCE_TIME); + }); } const ANSWERS = new Answers(); @@ -162,13 +741,13 @@

Source: answers-umd.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/core_analytics_analytics.js.html b/docs/core_analytics_analytics.js.html deleted file mode 100644 index a159cb716..000000000 --- a/docs/core_analytics_analytics.js.html +++ /dev/null @@ -1,61 +0,0 @@ - - - - - JSDoc: Source: core/analytics/analytics.js - - - - - - - - - - -
- -

Source: core/analytics/analytics.js

- - - - - - -
-
-
/** @module Analytics */
-
-/**
- * Interface for reporting Analytics
- */
-export default class Analytics {
-  constructor () {
-    console.log('this is the analytics module...');
-  }
-}
-
-
-
- - - - -
- - - -
- -
- Documentation generated by JSDoc 3.6.2 on Wed Jul 03 2019 10:34:41 GMT-0400 (Eastern Daylight Time) -
- - - - - diff --git a/docs/core_analytics_analyticsevent.js.html b/docs/core_analytics_analyticsevent.js.html index e8a0bb0ff..0a97749d4 100644 --- a/docs/core_analytics_analyticsevent.js.html +++ b/docs/core_analytics_analyticsevent.js.html @@ -31,8 +31,19 @@

Source: core/analytics/analyticsevent.js

*/ export default class AnalyticsEvent { constructor (type, label) { + /** + * The type of event to report + * @type {string} + */ this.eventType = type.toUpperCase(); - this.label = label; + + /** + * An optional label to be provided for the event + * @type {string} + */ + if (label) { + this.label = label; + } } /** @@ -41,6 +52,7 @@

Source: core/analytics/analyticsevent.js

*/ addOptions (options) { Object.assign(this, options); + return this; } /** @@ -49,6 +61,17 @@

Source: core/analytics/analyticsevent.js

toApiEvent () { return Object.assign({}, this); } + + /** + * Creating an analytics event from raw data. + * @param {Object} data + */ + static fromData (data) { + const { type, label, ...eventOptions } = data; + const analyticsEvent = new AnalyticsEvent(type, label); + analyticsEvent.addOptions(eventOptions); + return analyticsEvent; + } }
@@ -60,13 +83,13 @@

Source: core/analytics/analyticsevent.js


diff --git a/docs/core_analytics_analyticsreporter.js.html b/docs/core_analytics_analyticsreporter.js.html index 2365c5e44..1d4593c12 100644 --- a/docs/core_analytics_analyticsreporter.js.html +++ b/docs/core_analytics_analyticsreporter.js.html @@ -28,55 +28,99 @@

Source: core/analytics/analyticsreporter.js

/** @module AnalyticsReporter */
 
-import ApiRequest from '../http/apirequest';
 import AnalyticsEvent from './analyticsevent';
 import { AnswersAnalyticsError } from '../errors/errors';
-import { ANALYTICS_BASE_URL } from '../constants';
+import { PRODUCTION } from '../constants';
+import HttpRequester from '../http/httprequester';
+import { getAnalyticsUrl } from '../utils/urlutils';
+
+/** @typedef {import('../services/analyticsreporterservice').default} AnalyticsReporterService */
 
 /**
- * Class for reporting analytics events to the server
+ * Class for reporting analytics events to the server via HTTP
+ *
+ * @implements {AnalyticsReporterService}
  */
 export default class AnalyticsReporter {
-  constructor (apiKey, answersKey) {
-    this._apiKey = apiKey;
-    this._answersKey = answersKey;
-
-    // TODO(jdelerme): Temporary workaround for getting internal business ID for the analytics endpoint
-    // To be removed when the endpoint is moved behind liveapi
-    const businessIdReq = new ApiRequest({
-      endpoint: '/v2/accounts/me/answers/query',
-      apiKey: this._apiKey,
-      version: 20190301,
-      params: {
-        'input': '',
-        'answersKey': this._answersKey
-      }
-    });
-
-    businessIdReq.get().then(r => r.json()).then(d => {
-      this._businessId = d.response.businessId;
-    });
+  constructor (
+    experienceKey,
+    experienceVersion,
+    businessId,
+    globalOptions = {},
+    environment = PRODUCTION) {
+    /**
+     * The internal business identifier used for reporting
+     * @type {number}
+     */
+    this._businessId = businessId;
+
+    /**
+     * Options to include with every analytic event reported to the server
+     * @type {object}
+     * @private
+     */
+    this._globalOptions = Object.assign({}, globalOptions, { experienceKey });
+
+    /**
+     * The environment of the Answers experience
+     * @type {string}
+     * @private
+     */
+    this._environment = environment;
+
+    /**
+     * Base URL for the analytics API
+     * @type {string}
+     * @private
+     */
+    this._baseUrl = getAnalyticsUrl(this._environment);
+
+    /**
+     * Boolean indicating if opted in or out of conversion tracking
+     * @type {boolean}
+     * @private
+     */
+    this._conversionTrackingEnabled = false;
+
+    if (experienceVersion) {
+      this._globalOptions.experienceVersion = experienceVersion;
+    }
   }
 
+  getQueryId () {
+    return this._globalOptions.queryId;
+  }
+
+  setQueryId (queryId) {
+    this._globalOptions.queryId = queryId;
+  }
+
+  /** @inheritdoc */
   report (event) {
+    let cookieData = {};
+    if (this._conversionTrackingEnabled && typeof ytag === 'function') {
+      ytag('optin', true);
+      cookieData = ytag('yfpc', null);
+    } else if (this._conversionTrackingEnabled) {
+      throw new AnswersAnalyticsError('Tried to enable conversion tracking without including ytag');
+    }
+
     if (!(event instanceof AnalyticsEvent)) {
       throw new AnswersAnalyticsError('Tried to send invalid analytics event', event);
     }
 
-    event.addOptions({ answersKey: this._answersKey });
+    event.addOptions(this._globalOptions);
 
-    const request = new ApiRequest({
-      baseUrl: ANALYTICS_BASE_URL,
-      endpoint: `/realtimeanalytics/data/answers/${this._businessId}`,
-      apiKey: this._apiKey,
-      version: 20190301,
-      params: {
-        'data': event.toApiEvent()
-      }
-    });
+    return new HttpRequester().beacon(
+      `${this._baseUrl}/realtimeanalytics/data/answers/${this._businessId}`,
+      { data: event.toApiEvent(), ...cookieData }
+    );
+  }
 
-    request.post()
-      .catch(console.err);
+  /** @inheritdoc */
+  setConversionTrackingEnabled (isEnabled) {
+    this._conversionTrackingEnabled = isEnabled;
+    this._baseUrl = getAnalyticsUrl(this._environment, isEnabled);
   }
 }
 
@@ -89,13 +133,13 @@

Source: core/analytics/analyticsreporter.js


diff --git a/docs/core_analytics_noopanalyticsreporter.js.html b/docs/core_analytics_noopanalyticsreporter.js.html new file mode 100644 index 000000000..598068bc0 --- /dev/null +++ b/docs/core_analytics_noopanalyticsreporter.js.html @@ -0,0 +1,65 @@ + + + + + JSDoc: Source: core/analytics/noopanalyticsreporter.js + + + + + + + + + + +
+ +

Source: core/analytics/noopanalyticsreporter.js

+ + + + + + +
+
+
/** @typedef {import('../services/analyticsreporterservice').default} AnalyticsReporterService */
+
+/**
+ * @implements {AnalyticsReporterService}
+ */
+export default class NoopAnalyticsReporter {
+  /** @inheritdoc */
+  report (event) {
+    return true;
+  }
+
+  /** @inheritdoc */
+  setConversionTrackingEnabled (isEnabled) {}
+}
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/docs/core_constants.js.html b/docs/core_constants.js.html index 8e1d76c5a..c816fd0a2 100644 --- a/docs/core_constants.js.html +++ b/docs/core_constants.js.html @@ -28,17 +28,32 @@

Source: core/constants.js

/** @module */
 
-/** The current lib version, reported with errors and analytics */
-export const LIB_VERSION = 'v0.7.3';
+/** The current lib version, reported with errors and analytics, injected by the build process */
+export const LIB_VERSION = '@@LIB_VERSION';
 
-/** The base url for the api backend */
-export const API_BASE_URL = 'https://liveapi.yext.com';
+/** The current locale, injected by the build process */
+export const LOCALE = '@@LOCALE';
 
-/** The default url for compiled component templates */
-export const COMPILED_TEMPLATES_URL = 'https://assets.sitescdn.net/answers/answerstemplates.compiled.min.js';
+/** The identifier of the production environment */
+export const PRODUCTION = 'production';
+
+/** The identifier of the sandbox environment */
+export const SANDBOX = 'sandbox';
 
-/** The base url for the analytics backend  */
-export const ANALYTICS_BASE_URL = 'https://realtimeanalytics.yext.com';
+/** The default url for compiled component templates */
+export const COMPILED_TEMPLATES_URL = `https://assets.sitescdn.net/answers/${LIB_VERSION}/answerstemplates.compiled.min.js`;
+
+/** The query source, reported with analytics */
+export const QUERY_SOURCE = 'STANDARD';
+
+export const ENDPOINTS = {
+  UNIVERSAL_SEARCH: '/v2/accounts/me/answers/query',
+  VERTICAL_SEARCH: '/v2/accounts/me/answers/vertical/query',
+  QUESTION_SUBMISSION: '/v2/accounts/me/createQuestion',
+  UNIVERSAL_AUTOCOMPLETE: '/v2/accounts/me/answers/autocomplete',
+  VERTICAL_AUTOCOMPLETE: '/v2/accounts/me/answers/vertical/autocomplete',
+  FILTER_SEARCH: '/v2/accounts/me/answers/filtersearch'
+};
 
@@ -49,13 +64,13 @@

Source: core/constants.js


diff --git a/docs/core_core.js.html b/docs/core_core.js.html index afc19dabc..72e64ff58 100644 --- a/docs/core_core.js.html +++ b/docs/core_core.js.html @@ -27,42 +27,58 @@

Source: core/core.js

/** @module Core */
-
-import SearchApi from './search/searchapi';
-import AutoCompleteApi from './search/autocompleteapi';
+import { provideCore } from '@yext/answers-core/lib/commonjs';
 
 import SearchDataTransformer from './search/searchdatatransformer';
 
-import Storage from './storage/storage';
+import VerticalResults from './models/verticalresults';
+import UniversalResults from './models/universalresults';
+import QuestionSubmission from './models/questionsubmission';
+import Navigation from './models/navigation';
+import AlternativeVerticals from './models/alternativeverticals';
+import LocationBias from './models/locationbias';
+import QueryTriggers from './models/querytriggers';
+
 import StorageKeys from './storage/storagekeys';
+import AnalyticsEvent from './analytics/analyticsevent';
+import FilterRegistry from './filters/filterregistry';
+import DirectAnswer from './models/directanswer';
+import AutoCompleteResponseTransformer from './search/autocompleteresponsetransformer';
+
+import { PRODUCTION, ENDPOINTS } from './constants';
+import { getCachedLiveApiUrl, getLiveApiUrl, getKnowledgeApiUrl } from './utils/urlutils';
+import { SearchParams } from '../ui';
+import SearchStates from './storage/searchstates';
+
+/** @typedef {import('./storage/storage').default} Storage */
 
 /**
  * Core is the main application container for all of the network and storage
- * related behaviors of the application.
+ * related behaviors of the application. It uses an instance of the external Core
+ * library to perform the actual network calls.
  */
 export default class Core {
-  constructor (opts = {}) {
-    if (typeof opts.apiKey !== 'string') {
-      throw new Error('Missing required `apiKey`. Type must be {string}');
-    }
-
-    if (typeof opts.answersKey !== 'string') {
-      throw new Error('Missing required `answersKey`. Type must be {string}');
-    }
-
+  constructor (config = {}) {
     /**
      * A reference to the client API Key used for all requests
      * @type {string}
      * @private
      */
-    this._apiKey = opts.apiKey;
+    this._apiKey = config.apiKey;
 
     /**
      * A reference to the client Answers Key used for all requests
      * @type {string}
      * @private
      */
-    this._answersKey = opts.answersKey;
+    this._experienceKey = config.experienceKey;
+
+    /**
+     * The answers config version to use for all requests
+     * @type {string}
+     * @private
+     */
+    this._experienceVersion = config.experienceVersion;
 
     /**
      * A reference to the client locale used for all requests. If not specified, defaults to "en" (for
@@ -70,61 +86,347 @@ 

Source: core/core.js

* @type {string} * @private */ - this._locale = opts.locale || 'en'; + this._locale = config.locale; + + /** + * A map of field formatters used to format results, if present + * @type {Object<string, function>} + * @private + */ + this._fieldFormatters = config.fieldFormatters || {}; /** * A reference to the core data storage that powers the UI * @type {Storage} - * @private */ - this.storage = new Storage(); + this.storage = config.storage; /** - * An abstraction containing the integration with the RESTful search API - * For both vertical and universal search - * @type {Search} - * @private + * The filterRegistry is in charge of setting, removing, and retrieving filters + * and facet filters from storage. + * @type {FilterRegistry} */ - this._searcher = new SearchApi({ - apiKey: this._apiKey, - answersKey: this._answersKey, - locale: this._locale - }); + this.filterRegistry = new FilterRegistry(this.storage); /** - * An abstraction containing the integration with the RESTful autocomplete API - * For filter search, vertical autocomplete, and universal autocomplete - * @type {Autocomplete} - * @private + * A local reference to the analytics reporter, used to report events for this component + * @type {AnalyticsReporter} */ - this._autoComplete = new AutoCompleteApi({ + this._analyticsReporter = config.analyticsReporter; + + /** + * A user-given function that returns an analytics event to fire after a universal search. + * @type {Function} + */ + this.onUniversalSearch = config.onUniversalSearch || function () {}; + + /** + * A user-given function that returns an analytics event to fire after a vertical search. + * @type {Function} + */ + this.onVerticalSearch = config.onVerticalSearch || function () {}; + + /** + * The environment which determines which URLs the requests use. + * @type {string} + */ + this._environment = config.environment || PRODUCTION; + + /** + * @type {string} + */ + this._verticalKey = config.verticalKey; + + /** + * @type {ComponentManager} + */ + this._componentManager = config.componentManager; + } + + /** + * Sets a reference in core to the global QueryUpdateListener. + * + * @param {QueryUpdateListener} queryUpdateListener + */ + setQueryUpdateListener (queryUpdateListener) { + this.queryUpdateListener = queryUpdateListener; + } + + /** + * Initializes the {@link Core} by providing it with an instance of the Core library. + */ + init () { + const params = { apiKey: this._apiKey, - answersKey: this._answersKey, - locale: this._locale - }); + experienceKey: this._experienceKey, + locale: this._locale, + experienceVersion: this._experienceVersion, + endpoints: this._getServiceUrls() + }; + + this._coreLibrary = provideCore(params); + } + + /** + * Get the urls for each service based on the environment. + */ + _getServiceUrls () { + return { + universalSearch: getLiveApiUrl(this._environment) + ENDPOINTS.UNIVERSAL_SEARCH, + verticalSearch: getLiveApiUrl(this._environment) + ENDPOINTS.VERTICAL_SEARCH, + questionSubmission: getKnowledgeApiUrl(this._environment) + ENDPOINTS.QUESTION_SUBMISSION, + universalAutocomplete: getCachedLiveApiUrl(this._environment) + ENDPOINTS.UNIVERSAL_AUTOCOMPLETE, + verticalAutocomplete: getCachedLiveApiUrl(this._environment) + ENDPOINTS.VERTICAL_AUTOCOMPLETE, + filterSearch: getCachedLiveApiUrl(this._environment) + ENDPOINTS.FILTER_SEARCH + }; + } + + /** + * @returns {boolean} A boolean indicating if the {@link Core} has been + * initailized. + */ + isInitialized () { + return !!this._coreLibrary; } - verticalSearch (queryString, verticalKey, filter) { - return this._searcher - .verticalQuery(queryString, verticalKey, filter) - .then(response => SearchDataTransformer.transformVertical(response)) + /** + * Search in the context of a vertical + * @param {string} verticalKey vertical ID for the search + * @param {Object} options additional settings for the search. + * @param {boolean} options.useFacets Whether to apply facets to this search, or to reset them instead + * @param {boolean} options.resetPagination Whether to reset the search offset, going back to page 1. + * @param {boolean} options.setQueryParams Whether to persist certain params in the url + * @param {string} options.sendQueryId Whether to send the queryId currently in storage. + * If paging within a query, the same ID should be used. + * @param {Object} query The query details + * @param {string} query.input The input to search for + * @param {boolean} query.append If true, adds the results of this query to the end of the current results, defaults false + */ + verticalSearch (verticalKey, options = {}, query = {}) { + window.performance.mark('yext.answers.verticalQueryStart'); + if (!query.append) { + const verticalResults = this.storage.get(StorageKeys.VERTICAL_RESULTS); + if (!verticalResults || verticalResults.searchState !== SearchStates.SEARCH_LOADING) { + this.storage.set(StorageKeys.VERTICAL_RESULTS, VerticalResults.searchLoading()); + } + this.storage.set(StorageKeys.SPELL_CHECK, {}); + this.storage.set(StorageKeys.LOCATION_BIAS, LocationBias.searchLoading()); + } + + const { resetPagination, useFacets, sendQueryId, setQueryParams } = options; + if (resetPagination) { + this.storage.delete(StorageKeys.SEARCH_OFFSET); + } + + if (!useFacets) { + this.filterRegistry.setFacetFilterNodes([], []); + } + + const context = this.storage.get(StorageKeys.API_CONTEXT); + const referrerPageUrl = this.storage.get(StorageKeys.REFERRER_PAGE_URL); + + const defaultQueryInput = this.storage.get(StorageKeys.QUERY) || ''; + const parsedQuery = Object.assign({}, { input: defaultQueryInput }, query); + + if (setQueryParams) { + if (context) { + this.storage.setWithPersist(StorageKeys.API_CONTEXT, context); + } + if (referrerPageUrl !== undefined) { + this.storage.setWithPersist(StorageKeys.REFERRER_PAGE_URL, referrerPageUrl); + } + } + + const searchConfig = this.storage.get(StorageKeys.SEARCH_CONFIG) || {}; + if (!searchConfig.verticalKey) { + this.storage.set(StorageKeys.SEARCH_CONFIG, { + ...searchConfig, + verticalKey: verticalKey + }); + } + const locationRadius = this._getLocationRadius(); + const queryTrigger = this.storage.get(StorageKeys.QUERY_TRIGGER); + const queryTriggerForApi = this.getQueryTriggerForSearchApi(queryTrigger); + + return this._coreLibrary + .verticalSearch({ + verticalKey: verticalKey || searchConfig.verticalKey, + limit: this.storage.get(StorageKeys.SEARCH_CONFIG).limit, + location: this._getLocationPayload(), + query: parsedQuery.input, + queryId: sendQueryId && this.storage.get(StorageKeys.QUERY_ID), + retrieveFacets: this._isDynamicFiltersEnabled, + facets: this.filterRegistry.getFacetsPayload(), + staticFilters: this.filterRegistry.getStaticFilterPayload(), + offset: this.storage.get(StorageKeys.SEARCH_OFFSET) || 0, + skipSpellCheck: this.storage.get(StorageKeys.SKIP_SPELL_CHECK), + queryTrigger: queryTriggerForApi, + sessionTrackingEnabled: this.storage.get(StorageKeys.SESSIONS_OPT_IN).value, + sortBys: this.storage.get(StorageKeys.SORT_BYS), + /** In the SDK a locationRadius of 0 means "unset my locationRadius" */ + locationRadius: locationRadius === 0 ? undefined : locationRadius, + context: context, + referrerPageUrl: referrerPageUrl, + querySource: this.storage.get(StorageKeys.QUERY_SOURCE) + }) + .then(response => SearchDataTransformer.transformVertical(response, this._fieldFormatters, verticalKey)) .then(data => { + this._persistFacets(); + this._persistFilters(); + this._persistLocationRadius(); + + this.storage.set(StorageKeys.QUERY_ID, data[StorageKeys.QUERY_ID]); this.storage.set(StorageKeys.NAVIGATION, data[StorageKeys.NAVIGATION]); - this.storage.set(StorageKeys.VERTICAL_RESULTS, data[StorageKeys.VERTICAL_RESULTS]); + this.storage.set(StorageKeys.ALTERNATIVE_VERTICALS, data[StorageKeys.ALTERNATIVE_VERTICALS]); + + if (query.append) { + const mergedResults = this.storage.get(StorageKeys.VERTICAL_RESULTS) + .append(data[StorageKeys.VERTICAL_RESULTS]); + this.storage.set(StorageKeys.VERTICAL_RESULTS, mergedResults); + } else { + this.storage.set(StorageKeys.VERTICAL_RESULTS, data[StorageKeys.VERTICAL_RESULTS]); + } + + if (data[StorageKeys.DYNAMIC_FILTERS]) { + this.storage.set(StorageKeys.DYNAMIC_FILTERS, data[StorageKeys.DYNAMIC_FILTERS]); + this.storage.set(StorageKeys.RESULTS_HEADER, data[StorageKeys.DYNAMIC_FILTERS]); + } + if (data[StorageKeys.SPELL_CHECK]) { + this.storage.set(StorageKeys.SPELL_CHECK, data[StorageKeys.SPELL_CHECK]); + } + if (data[StorageKeys.LOCATION_BIAS]) { + this.storage.set(StorageKeys.LOCATION_BIAS, data[StorageKeys.LOCATION_BIAS]); + } + this.storage.delete(StorageKeys.SKIP_SPELL_CHECK); + this.storage.delete(StorageKeys.QUERY_TRIGGER); + + const exposedParams = { + verticalKey: verticalKey, + queryString: parsedQuery.input, + resultsCount: this.storage.get(StorageKeys.VERTICAL_RESULTS).resultsCount, + resultsContext: data[StorageKeys.VERTICAL_RESULTS].resultsContext + }; + const analyticsEvent = this.onVerticalSearch(exposedParams); + if (typeof analyticsEvent === 'object') { + this._analyticsReporter.report(AnalyticsEvent.fromData(analyticsEvent)); + } + this.updateHistoryAfterSearch(queryTrigger); + window.performance.mark('yext.answers.verticalQueryResponseRendered'); }); } - search (queryString, urls) { - return this._searcher - .query(queryString) - .then(response => SearchDataTransformer.transform(response, urls)) + clearResults () { + this.storage.set(StorageKeys.QUERY, null); + this.storage.set(StorageKeys.QUERY_ID, ''); + this.storage.set(StorageKeys.RESULTS_HEADER, {}); + this.storage.set(StorageKeys.SPELL_CHECK, {}); // TODO has a model but not cleared w new + this.storage.set(StorageKeys.DYNAMIC_FILTERS, {}); // TODO has a model but not cleared w new + this.storage.set(StorageKeys.QUESTION_SUBMISSION, new QuestionSubmission({})); + this.storage.set(StorageKeys.NAVIGATION, new Navigation()); + this.storage.set(StorageKeys.ALTERNATIVE_VERTICALS, new AlternativeVerticals({})); + this.storage.set(StorageKeys.DIRECT_ANSWER, new DirectAnswer({})); + this.storage.set(StorageKeys.LOCATION_BIAS, new LocationBias({})); + this.storage.set(StorageKeys.VERTICAL_RESULTS, new VerticalResults({})); + this.storage.set(StorageKeys.UNIVERSAL_RESULTS, new UniversalResults({})); + } + + /** + * Page within the results of the last query + */ + verticalPage () { + this.triggerSearch(QueryTriggers.PAGINATION); + } + + search (queryString, options = {}) { + const urls = this._getUrls(queryString); + window.performance.mark('yext.answers.universalQueryStart'); + const { setQueryParams } = options; + const context = this.storage.get(StorageKeys.API_CONTEXT); + const referrerPageUrl = this.storage.get(StorageKeys.REFERRER_PAGE_URL); + + if (setQueryParams) { + if (context) { + this.storage.setWithPersist(StorageKeys.API_CONTEXT, context); + } + if (referrerPageUrl !== undefined) { + this.storage.setWithPersist(StorageKeys.REFERRER_PAGE_URL, referrerPageUrl); + } + } + + this.storage.set(StorageKeys.DIRECT_ANSWER, {}); + const universalResults = this.storage.get(StorageKeys.UNIVERSAL_RESULTS); + if (!universalResults || universalResults.searchState !== SearchStates.SEARCH_LOADING) { + this.storage.set(StorageKeys.UNIVERSAL_RESULTS, UniversalResults.searchLoading()); + } + this.storage.set(StorageKeys.QUESTION_SUBMISSION, {}); + this.storage.set(StorageKeys.SPELL_CHECK, {}); + this.storage.set(StorageKeys.LOCATION_BIAS, LocationBias.searchLoading()); + + const queryTrigger = this.storage.get(StorageKeys.QUERY_TRIGGER); + const queryTriggerForApi = this.getQueryTriggerForSearchApi(queryTrigger); + return this._coreLibrary + .universalSearch({ + query: queryString, + location: this._getLocationPayload(), + skipSpellCheck: this.storage.get(StorageKeys.SKIP_SPELL_CHECK), + queryTrigger: queryTriggerForApi, + sessionTrackingEnabled: this.storage.get(StorageKeys.SESSIONS_OPT_IN).value, + context: context, + referrerPageUrl: referrerPageUrl, + querySource: this.storage.get(StorageKeys.QUERY_SOURCE) + }) + .then(response => SearchDataTransformer.transformUniversal(response, urls, this._fieldFormatters)) .then(data => { + this.storage.set(StorageKeys.QUERY_ID, data[StorageKeys.QUERY_ID]); this.storage.set(StorageKeys.NAVIGATION, data[StorageKeys.NAVIGATION]); this.storage.set(StorageKeys.DIRECT_ANSWER, data[StorageKeys.DIRECT_ANSWER]); - this.storage.set(StorageKeys.UNIVERSAL_RESULTS, data[StorageKeys.UNIVERSAL_RESULTS], urls); + this.storage.set(StorageKeys.UNIVERSAL_RESULTS, data[StorageKeys.UNIVERSAL_RESULTS]); + this.storage.set(StorageKeys.SPELL_CHECK, data[StorageKeys.SPELL_CHECK]); + this.storage.set(StorageKeys.LOCATION_BIAS, data[StorageKeys.LOCATION_BIAS]); + + this.storage.delete(StorageKeys.SKIP_SPELL_CHECK); + this.storage.delete(StorageKeys.QUERY_TRIGGER); + + const exposedParams = this._getOnUniversalSearchParams( + data[StorageKeys.UNIVERSAL_RESULTS].sections, + queryString); + const analyticsEvent = this.onUniversalSearch(exposedParams); + if (typeof analyticsEvent === 'object') { + this._analyticsReporter.report(AnalyticsEvent.fromData(analyticsEvent)); + } + this.updateHistoryAfterSearch(queryTrigger); + window.performance.mark('yext.answers.universalQueryResponseRendered'); }); } + /** + * Builds the object passed as a parameter to onUniversalSearch. This object + * contains information about the universal search's query and result counts. + * + * @param {Array<Section>} sections The sections of results. + * @param {string} queryString The search query. + * @return {Object<string, ?>} + */ + _getOnUniversalSearchParams (sections, queryString) { + const resultsCountByVertical = sections.reduce( + (resultsCountMap, section) => { + const { verticalConfigId, resultsCount, results } = section; + resultsCountMap[verticalConfigId] = { + totalResultsCount: resultsCount, + displayedResultsCount: results.length + }; + return resultsCountMap; + }, + {}); + const exposedParams = { + queryString, + sectionsCount: sections.length, + resultsCountByVertical + }; + + return exposedParams; + } + /** * Given an input, query for a list of similar results and set into storage * @@ -132,10 +434,15 @@

Source: core/core.js

* @param {string} namespace the namespace to use for the storage key */ autoCompleteUniversal (input, namespace) { - return this._autoComplete - .queryUniversal(input) + return this._coreLibrary + .universalAutocomplete({ + input: input, + sessionTrackingEnabled: this.storage.get(StorageKeys.SESSIONS_OPT_IN).value + }) + .then(response => AutoCompleteResponseTransformer.transformAutoCompleteResponse(response)) .then(data => { this.storage.set(`${StorageKeys.AUTOCOMPLETE}.${namespace}`, data); + return data; }); } @@ -146,52 +453,323 @@

Source: core/core.js

* @param {string} input the string to autocomplete * @param {string} namespace the namespace to use for the storage key * @param {string} verticalKey the vertical key for the experience - * @param {string} barKey the bar key for the experience */ - autoCompleteVertical (input, namespace, verticalKey, barKey) { - return this._autoComplete - .queryVertical(input, verticalKey, barKey) + autoCompleteVertical (input, namespace, verticalKey) { + return this._coreLibrary + .verticalAutocomplete({ + input: input, + verticalKey: verticalKey, + sessionTrackingEnabled: this.storage.get(StorageKeys.SESSIONS_OPT_IN).value + }) + .then(response => AutoCompleteResponseTransformer.transformAutoCompleteResponse(response)) .then(data => { this.storage.set(`${StorageKeys.AUTOCOMPLETE}.${namespace}`, data); + return data; }); } /** * Given an input, provide a list of suitable filters for autocompletion * - * @param {string} input the string to search for filters with - * @param {string} namespace the namespace to use for the storage key - * @param {string} verticalKey the vertical key for the experience - * @param {string} barKey the bar key for the experience - */ - autoCompleteFilter (input, namespace, verticalKey, barKey) { - return this._autoComplete - .queryFilter(input, verticalKey, barKey) + * @param {string} input the string to search for filters with + * @param {object} config the config to serach for filters with + * @param {string} config.namespace the namespace to use for the storage key + * @param {string} config.verticalKey the vertical key for the config + * @param {object} config.searchParameters the search parameters for the config v2 + */ + autoCompleteFilter (input, config) { + const searchParamFields = config.searchParameters.fields.map(field => ({ + fieldApiName: field.fieldId, + entityType: field.entityTypeId, + fetchEntities: field.shouldFetchEntities + })); + return this._coreLibrary + .filterSearch({ + input: input, + verticalKey: config.verticalKey, + fields: searchParamFields, + sectioned: config.searchParameters.sectioned, + sessionTrackingEnabled: this.storage.get(StorageKeys.SESSIONS_OPT_IN).value + }) + .then(response => AutoCompleteResponseTransformer.transformFilterSearchResponse(response)) .then(data => { - this.storage.set(`${StorageKeys.AUTOCOMPLETE}.${namespace}`, data); + this.storage.set(`${StorageKeys.AUTOCOMPLETE}.${config.namespace}`, data); + }); + } + + /** + * Submits a question to the server and updates the underlying question model + * @param {object} question The question object to submit to the server + * @param {number} question.entityId The entity to associate with the question (required) + * @param {string} question.site The "publisher" of the (e.g. 'FIRST_PARTY') + * @param {string} question.name The name of the author + * @param {string} question.email The email address of the author + * @param {string} question.questionText The question + * @param {string} question.questionDescription Additional information about the question + */ + submitQuestion (question) { + return this._coreLibrary + .submitQuestion({ + ...question, + sessionTrackingEnabled: this.storage.get(StorageKeys.SESSIONS_OPT_IN).value + }) + .then(() => { + this.storage.set( + StorageKeys.QUESTION_SUBMISSION, + QuestionSubmission.submitted()); }); } + /** + * Stores the given sortBy into storage, to be used for the next search + * @param {Object} sortByOptions + */ + setSortBys (...sortByOptions) { + const sortBys = sortByOptions.map(option => { + return { + type: option.type, + field: option.field, + direction: option.direction + }; + }); + this.storage.setWithPersist(StorageKeys.SORT_BYS, sortBys); + } + + /** + * Clears the sortBys key in storage. + */ + clearSortBys () { + this.storage.delete(StorageKeys.SORT_BYS); + } + /** * Stores the given query into storage, to be used for the next search * @param {string} query the query to store */ setQuery (query) { - this.storage.set(StorageKeys.QUERY, query); + this.storage.setWithPersist(StorageKeys.QUERY, query); + } + + /** + * Stores the provided query ID, to be used in analytics + * @param {string} queryId The query id to store + */ + setQueryId (queryId) { + this.storage.set(StorageKeys.QUERY_ID, queryId); + } + + triggerSearch (queryTrigger, newQuery) { + const query = newQuery !== undefined + ? newQuery + : this.storage.get(StorageKeys.QUERY) || ''; + queryTrigger + ? this.storage.set(StorageKeys.QUERY_TRIGGER, queryTrigger) + : this.storage.delete(StorageKeys.QUERY_TRIGGER); + this.setQuery(query); + } + + /** + * Get all of the {@link FilterNode}s for static filters. + * @returns {Array<FilterNode>} + */ + getStaticFilterNodes () { + return this.filterRegistry.getStaticFilterNodes(); + } + + /** + * Get all of the active {@link FilterNode}s for facets. + * @returns {Array<FilterNode>} + */ + getFacetFilterNodes () { + return this.filterRegistry.getFacetFilterNodes(); } /** - * Stores the given filter into storage, to be used for the next search + * Get the {@link FilterNode} affecting the locationRadius url parameter. + * @returns {FilterNode} + */ + getLocationRadiusFilterNode () { + return this.filterRegistry.getFilterNodeByKey(StorageKeys.LOCATION_RADIUS_FILTER_NODE); + } + + /** + * Sets the filter nodes used for the current facet filters. * - * @param {string} namespace the namespace to use for the storage key - * @param {Filter} filter the filter to set + * Because the search response only sends back one + * set of facet filters, there can only be one active facet filter node + * at a time. + * @param {Array<string>} availableFieldIds + * @param {Array<FilterNode>} filterNodes + */ + setFacetFilterNodes (availableFieldids = [], filterNodes = []) { + this.filterRegistry.setFacetFilterNodes(availableFieldids, filterNodes); + } + + /** + * Sets the specified {@link FilterNode} under the given key. + * Will replace a preexisting node if there is one. + * @param {string} namespace + * @param {FilterNode} filterNode + */ + setStaticFilterNodes (namespace, filterNode) { + this.filterRegistry.setStaticFilterNodes(namespace, filterNode); + } + + /** + * Sets the locationRadius filterNode. + * @param {FilterNode} filterNode */ - setFilter (namespace, filter) { - this.storage.set(`${StorageKeys.FILTER}.${namespace}`, filter); + setLocationRadiusFilterNode (filterNode) { + this.filterRegistry.setLocationRadiusFilterNode(filterNode); } - on (evt, moduleId, cb) { - return this.storage.on(evt, moduleId, cb); + /** + * Remove the static FilterNode with this namespace. + * @param {string} namespace + */ + clearStaticFilterNode (namespace) { + this.filterRegistry.clearStaticFilterNode(namespace); + } + + /** + * Remove all facet FilterNodes. + */ + clearFacetFilterNodes () { + this.filterRegistry.clearFacetFilterNodes(); + } + + /** + * Clears the locationRadius filterNode. + */ + clearLocationRadiusFilterNode () { + this.filterRegistry.clearLocationRadiusFilterNode(); + } + + /** + * Gets the location object needed for answers-core + * + * @returns {LatLong|undefined} from answers-core + */ + _getLocationPayload () { + const geolocation = this.storage.get(StorageKeys.GEOLOCATION); + return geolocation && { + latitude: geolocation.lat, + longitude: geolocation.lng + }; + } + + /** + * Returns the query trigger for the search API given the SDK query trigger + * @param {QueryTriggers} queryTrigger SDK query trigger + * @returns {QueryTriggers} query trigger if accepted by the search API, null o/w + */ + getQueryTriggerForSearchApi (queryTrigger) { + if (![QueryTriggers.INITIALIZE, QueryTriggers.SUGGEST].includes(queryTrigger)) { + return null; + } + return queryTrigger; + } + + /** + * Depending on the QUERY_TRIGGER, either replaces the history state + * for searches on load/back navigation (INITIALIZE, SUGGEST, QUERY_PARAMETER), + * or pushes a new state. + * + * @param {QueryTriggers} queryTrigger SDK query trigger + * @returns {boolean} + */ + updateHistoryAfterSearch (queryTrigger) { + const replaceStateTriggers = [ + QueryTriggers.INITIALIZE, + QueryTriggers.SUGGEST, + QueryTriggers.QUERY_PARAMETER + ]; + if (replaceStateTriggers.includes(queryTrigger)) { + this.storage.replaceHistoryWithState(); + } else { + this.storage.pushStateToHistory(); + } + } + + /** + * Returns the current `locationRadius` state + * @returns {number|null} + */ + _getLocationRadius () { + const locationRadiusFilterNode = this.getLocationRadiusFilterNode(); + return locationRadiusFilterNode + ? locationRadiusFilterNode.getFilter().value + : null; + } + + /** + * Persists the current `facetFilters` state into the URL. + */ + _persistFacets () { + const persistedFacets = this.filterRegistry.createFacetsFromFilterNodes(); + this.storage.setWithPersist(StorageKeys.PERSISTED_FACETS, persistedFacets); + } + + /** + * Persists the current `filters` state into the URL. + */ + _persistFilters () { + const totalFilterNode = this.filterRegistry.getAllStaticFilterNodesCombined(); + const persistedFilter = totalFilterNode.getFilter(); + this.storage.setWithPersist(StorageKeys.PERSISTED_FILTER, persistedFilter); + } + + /** + * Persists the current `locationRadius` state into the URL. + */ + _persistLocationRadius () { + const locationRadius = this._getLocationRadius(); + if (locationRadius || locationRadius === 0) { + this.storage.setWithPersist(StorageKeys.PERSISTED_LOCATION_RADIUS, locationRadius); + } else { + this.storage.delete(StorageKeys.PERSISTED_LOCATION_RADIUS); + } + } + + enableDynamicFilters () { + this._isDynamicFiltersEnabled = true; + } + + on (evt, storageKey, cb) { + this.storage.registerListener({ + eventType: evt, + storageKey: storageKey, + callback: cb + }); + return this.storage; + } + + /** + * This is needed to support very old usages of the SDK that have not been updated + * to use StorageKeys.VERTICAL_PAGES_CONFIG + */ + _getUrls (query) { + let nav = this._componentManager.getActiveComponent('Navigation'); + if (!nav) { + return undefined; + } + + let tabs = nav.getState('tabs'); + let urls = {}; + + if (tabs && Array.isArray(tabs)) { + for (let i = 0; i < tabs.length; i++) { + let params = new SearchParams(tabs[i].url.split('?')[1]); + params.set('query', query); + + let url = tabs[i].baseUrl; + if (params.toString().length > 0) { + url += '?' + params.toString(); + } + urls[tabs[i].configId] = url; + } + } + return urls; } }
@@ -204,13 +782,13 @@

Source: core/core.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/core_errors_consoleerrorreporter.js.html b/docs/core_errors_consoleerrorreporter.js.html new file mode 100644 index 000000000..dcfa64d19 --- /dev/null +++ b/docs/core_errors_consoleerrorreporter.js.html @@ -0,0 +1,62 @@ + + + + + JSDoc: Source: core/errors/consoleerrorreporter.js + + + + + + + + + + +
+ +

Source: core/errors/consoleerrorreporter.js

+ + + + + + +
+
+
/** @typedef {import('../services/errorreporterservice').default} ErrorReporterService */
+
+/**
+ * @implements {ErrorReporterService}
+ */
+export default class ConsoleErrorReporter {
+  /** @inheritdoc */
+  report (err) {
+    console.error(err.toString());
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_errors_errorreporter.js.html b/docs/core_errors_errorreporter.js.html index db07895c6..96826be9e 100644 --- a/docs/core_errors_errorreporter.js.html +++ b/docs/core_errors_errorreporter.js.html @@ -28,32 +28,79 @@

Source: core/errors/errorreporter.js

/** @module ErrorReporter */
 
-import { AnswersBaseError } from './errors';
+import { AnswersBaseError, AnswersBasicError } from './errors';
 
 import ApiRequest from '../http/apirequest';
 import { LIB_VERSION } from '../constants';
 
+/** @typedef {import('../services/errorreporterservice').default} ErrorReporterService */
+
 /**
- * ErrorReporter is used for reporting errors to the server
+ * ErrorReporter is used for reporting errors to the console and API
+ *
+ * @implements {ErrorReporterService}
  */
 export default class ErrorReporter {
-  constructor (apiKey, answersKey) {
+  constructor (config, storage) {
     /**
      * The apiKey to use for reporting
      * @type {string}
      */
-    this.apiKey = apiKey;
+    this.apiKey = config.apiKey;
+
+    /**
+     * The experienceKey to use when reporting
+     * @type {string}
+     */
+    this.experienceKey = config.experienceKey;
+
+    /**
+     * The answers config version used for api requests
+     * @type {string|number}
+     */
+    this.experienceVersion = config.experienceVersion || 'config1.0';
+
+    /**
+     * If true, print entire error objects to the console for inspection
+     * @type {boolean}
+     */
+    this.printVerbose = config.printVerbose;
+
+    /**
+     * If true, report the error the server for logging and monitoring
+     * @type {boolean}
+     */
+    this.sendToServer = config.sendToServer;
 
     /**
-     * The answersKey to use when reporting
+     * The storage instance of the experience
+     * @type {Storage}
+     */
+    if (this.sendToServer && !storage) {
+      throw new AnswersBasicError(
+        'Must include storage to send errors to server',
+        'ErrorReporter');
+    }
+    this.storage = storage;
+
+    /**
+     * The environment of the Answers experience
      * @type {string}
+     * @private
      */
-    this.answersKey = answersKey;
+    this.environment = config.environment;
+
+    // Attach reporting listeners to window
+    window.addEventListener('error', e => this.report(e.error));
+    window.addEventListener('unhandledrejection', e => this.report(e.error));
   }
 
   /**
-   * report sends a network request to the server to be logged
-   * @param {AnswersBaseError} The error to be reported
+   * report pretty prints the error to the console, optionally
+   * prints the entire error if `printVerbose` is true, and sends the
+   * error to the server to be logged if `sendToServer` is true
+   * @param {AnswersBaseError} err The error to be reported
+   * @returns {AnswersBaseError} The reported error
    */
   report (err) {
     if (!(err instanceof AnswersBaseError) || err.reported) {
@@ -62,22 +109,47 @@ 

Source: core/errors/errorreporter.js

err.reported = true; - const request = new ApiRequest({ - endpoint: '/v2/accounts/me/answers/errors', - apiKey: this.apiKey, - version: 20190301, - params: { - 'error': err.toJson(), - 'libVersion': LIB_VERSION, - 'answersKey': this.answersKey - } - }); - - request.get() - .catch(console.err); + this.printError(err); + + if (this.sendToServer) { + const requestConfig = { + endpoint: '/v2/accounts/me/answers/errors', + apiKey: this.apiKey, + version: 20190301, + environment: this.environment, + params: { + 'libVersion': LIB_VERSION, + 'experienceVersion': this.experienceVersion, + 'experienceKey': this.experienceKey, + 'error': err.toJson() + } + }; + const request = new ApiRequest(requestConfig, this.storage); + + // TODO(amullings): We should probably change this endpoint to POST, + // ideally using the beacon API. Stack traces will likely easily hit URL + // length limits. + request.get() + .catch(console.err); + } return err; } + + /** + * prints the given error to the browser console + * @param {AnswersBaseError} err The error to be printed + */ + printError (err) { + if (this.printVerbose) { + console.error(`error: ${err.errorMessage} +code: ${err.errorCode} +boundary: ${err.boundary} +stack: ${err.stack}`); + } else { + console.error(err.toString()); + } + } }
@@ -89,13 +161,13 @@

Source: core/errors/errorreporter.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/core_errors_errors.js.html b/docs/core_errors_errors.js.html index f11807f14..5a5ef4500 100644 --- a/docs/core_errors_errors.js.html +++ b/docs/core_errors_errors.js.html @@ -40,19 +40,33 @@

Source: core/errors/errors.js

* 4XX errors: Core errors */ export class AnswersBaseError extends Error { - constructor (errorCode, message, boundary, causedBy) { + constructor (errorCode, message, boundary = 'unknown', causedBy) { super(message); this.errorCode = errorCode; this.errorMessage = message; this.boundary = boundary; - this.causedBy = causedBy; this.reported = false; + + if (causedBy) { + this.causedBy = causedBy instanceof AnswersBaseError + ? causedBy + : AnswersBaseError.from(causedBy); + this.stack = `${this.stack}\nCaused By: ${this.causedBy.stack}`; + } } toJson () { return JSON.stringify(this); } + toString () { + let string = `${this.errorMessage} (${this.boundary})`; + if (this.causedBy) { + string += `\n Caused By: ${this.causedBy.toString()}`; + } + return string; + } + static from (builtinError, boundary) { const error = new AnswersBasicError(builtinError.message, boundary); error.stack = builtinError.stack; @@ -71,6 +85,16 @@

Source: core/errors/errors.js

} } +/** + * AnswersUiError used for things like DOM errors. + * @extends AnswersBaseError + */ +export class AnswersConfigError extends AnswersBaseError { + constructor (message, boundary, causedBy) { + super(101, message, boundary, causedBy); + } +} + /** * AnswersUiError used for things like DOM errors. * @extends AnswersBaseError @@ -111,6 +135,7 @@

Source: core/errors/errors.js

super(400, message, boundary, causedBy); } } + /** * AnswersStorageError represents storage related errors * @extends AnswersBaseError @@ -123,6 +148,10 @@

Source: core/errors/errors.js

} } +/** + * AnswersAnalyticsError is used for errors when reporting analytics + * @extends AnswersBaseError + */ export class AnswersAnalyticsError extends AnswersBaseError { constructor (message, event, causedBy) { super(402, message, 'Analytics', causedBy); @@ -139,13 +168,13 @@

Source: core/errors/errors.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/core_eventemitter_eventemitter.js.html b/docs/core_eventemitter_eventemitter.js.html index 7b92a29bd..a5a147693 100644 --- a/docs/core_eventemitter_eventemitter.js.html +++ b/docs/core_eventemitter_eventemitter.js.html @@ -126,13 +126,13 @@

Source: core/eventemitter/eventemitter.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/core_filters_combinedfilternode.js.html b/docs/core_filters_combinedfilternode.js.html new file mode 100644 index 000000000..8dfb145a3 --- /dev/null +++ b/docs/core_filters_combinedfilternode.js.html @@ -0,0 +1,132 @@ + + + + + JSDoc: Source: core/filters/combinedfilternode.js + + + + + + + + + + +
+ +

Source: core/filters/combinedfilternode.js

+ + + + + + +
+
+
/** @module CombinedFilterNode */
+
+import Filter from '../models/filter';
+import FilterCombinators from './filtercombinators';
+import FilterNode from './filternode';
+
+/**
+ * A CombinedFilterNode represents a combined filter.
+ * A combined filter is a set of filters combined with a {@link FilterCombinators}
+ * ($and or $or). Since a combined filter is just a set of other filters,
+ * it does not have its own {@link FilterMetadata}, and its filter is dervied from
+ * its children.
+ */
+export default class CombinedFilterNode extends FilterNode {
+  constructor (filterNode = {}) {
+    super();
+    const { combinator, children } = filterNode;
+
+    /**
+     * @type {string}
+     */
+    this.combinator = combinator;
+
+    /**
+     * @type {Array<FilterNode>}
+     */
+    this.children = children || [];
+    Object.freeze(this);
+  }
+
+  /**
+   * Returns the filter created by combining this node's children.
+   * @type {Filter}
+   */
+  getFilter () {
+    const filters = this.children.map(childNode => childNode.getFilter());
+    switch (this.combinator) {
+      case (FilterCombinators.AND):
+        return Filter.and(...filters);
+      case (FilterCombinators.OR):
+        return Filter.or(...filters);
+    }
+    return Filter.empty();
+  }
+
+  /**
+   * Returns the metadata associated with this node's filter.
+   * Because a combined filter's purpose is solely to join together other filters,
+   * and does not have its own filter, this value is always null.
+   * @returns {null}
+   */
+  getMetadata () {
+    return null;
+  }
+
+  /**
+   * Returns this node's children.
+   * @returns {Array<FilterNode>}
+   */
+  getChildren () {
+    return this.children;
+  }
+
+  /**
+   * Recursively get all of the leaf SimpleFilterNodes.
+   * @returns {Array<SimpleFilterNode>}
+   */
+  getSimpleDescendants () {
+    return this.getChildren().flatMap(fn => fn.getSimpleDescendants());
+  }
+
+  /**
+   * Removes this filter node from the FilterRegistry by calling remove on each of its
+   * child FilterNodes.
+   */
+  remove () {
+    this.children.forEach(child => {
+      child.remove();
+    });
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_filters_filtercombinators.js.html b/docs/core_filters_filtercombinators.js.html new file mode 100644 index 000000000..05f3f934b --- /dev/null +++ b/docs/core_filters_filtercombinators.js.html @@ -0,0 +1,62 @@ + + + + + JSDoc: Source: core/filters/filtercombinators.js + + + + + + + + + + +
+ +

Source: core/filters/filtercombinators.js

+ + + + + + +
+
+
/** @module FilterCombinators */
+
+/**
+ * FilterCombinators are enums for valid ways to combine {@link Filter}s.
+ */
+const FilterCombinators = {
+  AND: '$and',
+  OR: '$or'
+};
+
+export default FilterCombinators;
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_filters_filtermetadata.js.html b/docs/core_filters_filtermetadata.js.html new file mode 100644 index 000000000..0f70d3696 --- /dev/null +++ b/docs/core_filters_filtermetadata.js.html @@ -0,0 +1,84 @@ + + + + + JSDoc: Source: core/filters/filtermetadata.js + + + + + + + + + + +
+ +

Source: core/filters/filtermetadata.js

+ + + + + + +
+
+
/** @module FilterMetadata */
+
+import FilterType from './filtertype';
+
+/**
+ * FilterMetadata is a container for additional display data for a {@link Filter}.
+ */
+export default class FilterMetadata {
+  constructor (metadata = {}) {
+    const { fieldName, displayValue, filterType } = metadata;
+
+    /**
+     * The display name for the field being filtered on.
+     * @type {string}
+     */
+    this.fieldName = fieldName;
+
+    /**
+     * The display value for the values being filtered on.
+     * Even if there are multiple values within the data of a filter,
+     * there should only be one display value for the whole filter.
+     * @type {string}
+     */
+    this.displayValue = displayValue;
+
+    /**
+     * What type of filter this is.
+     * @type {FilterType}
+     */
+    this.filterType = filterType || FilterType.STATIC;
+    Object.freeze(this);
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_filters_filternode.js.html b/docs/core_filters_filternode.js.html new file mode 100644 index 000000000..1576c9859 --- /dev/null +++ b/docs/core_filters_filternode.js.html @@ -0,0 +1,91 @@ + + + + + JSDoc: Source: core/filters/filternode.js + + + + + + + + + + +
+ +

Source: core/filters/filternode.js

+ + + + + + +
+
+
/** @module FilterNode */
+
+/**
+ * A FilterNode represents a single node in a filter tree.
+ * Each filter node has an associated filter, containing the filter
+ * data to send in a request, any additional filter metadata for display,
+ * and any children nodes.
+ *
+ * Implemented by {@link SimpleFilterNode} and {@link CombinedFilterNode}.
+ */
+export default class FilterNode {
+  /**
+   * Returns this node's filter.
+   * @returns {Filter}
+   */
+  getFilter () {}
+
+  /**
+   * Returns the metadata for this node's filter.
+   * @returns {FilterMetadata}
+   */
+  getMetadata () {}
+
+  /**
+   * Returns the children of this node.
+   * @returns {Array<FilterNode>}
+   */
+  getChildren () {}
+
+  /**
+   * Recursively get all of the leaf SimpleFilterNodes.
+   * @returns {Array<SimpleFilterNode>}
+   */
+  getSimpleDescendants () {}
+
+  /**
+   * Remove this FilterNode from the FilterRegistry.
+   */
+  remove () {}
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_filters_filternodefactory.js.html b/docs/core_filters_filternodefactory.js.html new file mode 100644 index 000000000..32ff8c4a8 --- /dev/null +++ b/docs/core_filters_filternodefactory.js.html @@ -0,0 +1,113 @@ + + + + + JSDoc: Source: core/filters/filternodefactory.js + + + + + + + + + + +
+ +

Source: core/filters/filternodefactory.js

+ + + + + + +
+
+
/** @module FilterNodeFactory */
+
+import FilterCombinators from './filtercombinators';
+import SimpleFilterNode from './simplefilternode';
+import CombinedFilterNode from './combinedfilternode';
+
+/**
+ * FilterNodeFactory is a class containing static helper methods for
+ * generating FilterNodes.
+ */
+export default class FilterNodeFactory {
+  /**
+   * Create an AND filter node, with specified children.
+   * @param  {...FilterNode} childrenNodes
+   * @returns {FilterNode}
+   */
+  static and (...childrenNodes) {
+    return FilterNodeFactory._combine(FilterCombinators.AND, childrenNodes);
+  }
+
+  /**
+   * Create an OR filter node, with specified children.
+   * @param  {...FilterNode} childrenNodes
+   * @returns {FilterNode}
+   */
+  static or (...childrenNodes) {
+    return FilterNodeFactory._combine(FilterCombinators.OR, childrenNodes);
+  }
+
+  /**
+   * Creates a combined filter node with the given combinator and children.
+   * @param {string} combinator
+   * @param {Array<FilterNode>} filterNodes
+   * @returns {FilterNode}
+   * @private
+   */
+  static _combine (combinator, filterNodes) {
+    const children = filterNodes.filter(fn => fn.getFilter().getFilterKey());
+    if (!children.length) {
+      return new SimpleFilterNode();
+    }
+    if (children.length === 1) {
+      return children[0];
+    }
+    return new CombinedFilterNode({
+      combinator: combinator,
+      children: children
+    });
+  }
+
+  /**
+   * Creates a filterNode from the given data.
+   * @param {Object|FilterNode} filterNode
+   * @returns {FilterNode}
+   */
+  static from (filterNode = {}) {
+    if (filterNode.children && filterNode.children.length) {
+      return new CombinedFilterNode(filterNode);
+    }
+    return new SimpleFilterNode(filterNode);
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_filters_filterregistry.js.html b/docs/core_filters_filterregistry.js.html new file mode 100644 index 000000000..17505a2af --- /dev/null +++ b/docs/core_filters_filterregistry.js.html @@ -0,0 +1,325 @@ + + + + + JSDoc: Source: core/filters/filterregistry.js + + + + + + + + + + +
+ +

Source: core/filters/filterregistry.js

+ + + + + + +
+
+
/** @module FilterRegistry */
+
+import FilterCombinators from './filtercombinators';
+import Facet from '../models/facet';
+import StorageKeys from '../storage/storagekeys';
+import FilterNodeFactory from './filternodefactory';
+
+/** @typedef {import('../storage/storage').default} Storage */
+
+/**
+ * FilterRegistry is a structure that manages static {@link Filter}s and {@link Facet} filters.
+ *
+ * Static filters and facet filters are stored within storage using FilterNodes.
+ */
+export default class FilterRegistry {
+  constructor (storage, availableFieldIds = []) {
+    /**
+     * FilterRegistry uses {@link Storage} for storing FilterNodes.
+     * Each node is given a unique key in storage.
+     * @type {Storage}
+     */
+    this.storage = storage;
+
+    /**
+     * All available field ids for the current facet filters, including
+     * field ids for unused but available filters.
+     * @type {Array<string>}
+     */
+    this.availableFieldIds = availableFieldIds;
+  }
+
+  /**
+   * Returns an array containing all of the filternodes stored in storage.
+   * @returns {Array<FilterNode>}
+   */
+  getAllFilterNodes () {
+    const storageFilterNodes = [
+      ...this.getStaticFilterNodes(),
+      ...this.getFacetFilterNodes()
+    ];
+    const locationRadiusFilterNode = this.getFilterNodeByKey(StorageKeys.LOCATION_RADIUS_FILTER_NODE);
+    if (locationRadiusFilterNode) {
+      storageFilterNodes.push(locationRadiusFilterNode);
+    }
+    return storageFilterNodes;
+  }
+
+  /**
+   * Get all of the {@link FilterNode}s for static filters.
+   * @returns {Array<FilterNode>}
+   */
+  getStaticFilterNodes () {
+    const staticFilterNodes = [];
+    this.storage.getAll().forEach((value, key) => {
+      if (key.startsWith(StorageKeys.STATIC_FILTER_NODES)) {
+        staticFilterNodes.push(value);
+      }
+    });
+    return staticFilterNodes;
+  }
+
+  /**
+   * Get all of the active {@link FilterNode}s for facets.
+   * @returns {Array<FilterNode>}
+   */
+  getFacetFilterNodes () {
+    return this.storage.get(StorageKeys.FACET_FILTER_NODES) || [];
+  }
+
+  /**
+   * Gets the static filters as a {@link Filter|CombinedFilter} to send to the answers-core
+   *
+   * @returns {CombinedFilter|Filter|null} Returns null if no filters with
+   *                                             filtering logic are present.
+   */
+  getStaticFilterPayload () {
+    const filterNodes = this.getStaticFilterNodes()
+      .filter(filterNode => {
+        return filterNode.getChildren().length > 0 || filterNode.getFilter().getFilterKey();
+      });
+    return filterNodes.length > 0
+      ? this._transformFilterNodes(filterNodes, FilterCombinators.AND)
+      : null;
+  }
+
+  /**
+   * Combines together all static filter nodes in the same shape that would
+   * be sent to the API.
+   *
+   * @returns {FilterNode}
+   */
+  getAllStaticFilterNodesCombined () {
+    const filterNodes = this.getStaticFilterNodes();
+    const totalNode = FilterNodeFactory.and(...filterNodes);
+    return totalNode;
+  }
+
+  /**
+   * Transforms a list of filter nodes {@link CombinedFilterNode} or {@link SimpleFilterNode} to
+   * answers-core's {@link Filter} or {@link CombinedFilter}
+   *
+   * @param {Array<CombinedFilterNode|SimpleFilterNode>} filterNodes
+   * @param {FilterCombinator} combinator from answers-core
+   * @returns {CombinedFilter|Filter} from answers-core
+   */
+  _transformFilterNodes (filterNodes, combinator) {
+    const filters = filterNodes.flatMap(filterNode => {
+      if (filterNode.children) {
+        return this._transformFilterNodes(filterNode.children, filterNode.combinator);
+      }
+
+      return this._transformSimpleFilterNode(filterNode);
+    });
+
+    return filters.length === 1
+      ? filters[0]
+      : {
+        filters: filters,
+        combinator: combinator
+      };
+  }
+
+  /**
+   * Transforms a {@link SimpleFilterNode} to answers-core's {@link Filter} or {@link CombinedFilter}
+   * if there are multiple matchers.
+   * TODO(SLAP-1183): remove the parsing for multiple matchers.
+   *
+   * @param {SimpleFilterNode} filterNode
+   * @returns {Filter}
+   */
+  _transformSimpleFilterNode (filterNode) {
+    const fieldId = Object.keys(filterNode.filter)[0];
+    const filterComparison = filterNode.filter[fieldId];
+    const matchers = Object.keys(filterComparison);
+    if (matchers.length === 1) {
+      const matcher = matchers[0];
+      const value = filterComparison[matcher];
+      return {
+        fieldId: fieldId,
+        matcher: matcher,
+        value: value
+      };
+    } else if (matchers.length > 1) {
+      const childFilters = matchers.map(matcher => ({
+        fieldId: fieldId,
+        matcher: matcher,
+        value: filterComparison[matcher]
+      }));
+      return {
+        combinator: FilterCombinators.AND,
+        filters: childFilters
+      };
+    }
+  }
+
+  /**
+   * Transforms a {@link Filter} into answers-core's {@link FacetOption}
+   *
+   * @param {Filter} filter
+   * @returns {FacetOption} from answers-core
+   */
+  _transformSimpleFilterNodeIntoFacetOption (filter) {
+    const fieldId = Object.keys(filter)[0];
+    const filterComparison = filter[fieldId];
+    const matcher = Object.keys(filterComparison)[0];
+    const value = filterComparison[matcher];
+    return {
+      matcher: matcher,
+      value: value
+    };
+  }
+
+  /**
+   * Combines the active facet FilterNodes into a single Facet
+   * @returns {Facet}
+   */
+  createFacetsFromFilterNodes () {
+    const getFilters = fn => fn.getChildren().length
+      ? fn.getChildren().flatMap(getFilters)
+      : fn.getFilter();
+    const filters = this.getFacetFilterNodes().flatMap(getFilters);
+    return Facet.fromFilters(this.availableFieldIds, ...filters);
+  }
+
+  /**
+   * Gets the facet filters as an array of Filters to send to the answers-core.
+   *
+   * @returns {Facet[]} from answers-core
+   */
+  getFacetsPayload () {
+    const hasFacetFilterNodes = this.storage.has(StorageKeys.FACET_FILTER_NODES);
+    const facets = hasFacetFilterNodes
+      ? this.createFacetsFromFilterNodes()
+      : this.storage.get(StorageKeys.PERSISTED_FACETS) || {};
+
+    const coreFacets = Object.entries(facets).map(([fieldId, filterArray]) => {
+      return {
+        fieldId: fieldId,
+        options: filterArray.map(this._transformSimpleFilterNodeIntoFacetOption)
+      };
+    });
+
+    return coreFacets;
+  }
+
+  /**
+   * Get the FilterNode with the corresponding key. Defaults to null.
+   * @param {string} key
+   */
+  getFilterNodeByKey (key) {
+    return this.storage.get(key);
+  }
+
+  /**
+   * Sets the specified {@link FilterNode} under the given key.
+   * Will replace a preexisting node if there is one.
+   * @param {string} key
+   * @param {FilterNode} filterNode
+   */
+  setStaticFilterNodes (key, filterNode) {
+    this.storage.set(`${StorageKeys.STATIC_FILTER_NODES}.${key}`, filterNode);
+  }
+
+  /**
+   * Sets the filter nodes used for the current facet filters.
+   *
+   * Because the search response only sends back one
+   * set of facet filters, there can only be one active facet filter node
+   * at a time.
+   * @param {Array<string>} availableFieldIds
+   * @param {Array<FilterNode>} filterNodes
+   */
+  setFacetFilterNodes (availableFieldIds = [], filterNodes = []) {
+    this.availableFieldIds = availableFieldIds;
+    this.storage.set(StorageKeys.FACET_FILTER_NODES, filterNodes);
+  }
+
+  /**
+   * Sets the locationRadius filterNode. There may only be one locationRadius active
+   * at a time.
+   * @param {FilterNode} filterNode
+   */
+  setLocationRadiusFilterNode (filterNode) {
+    this.storage.set(StorageKeys.LOCATION_RADIUS_FILTER_NODE, filterNode);
+  }
+
+  /**
+   * Deletes the static FilterNode with this namespace.
+   * @param {string} key
+   */
+  clearStaticFilterNode (key) {
+    this.storage.delete(`${StorageKeys.STATIC_FILTER_NODES}.${key}`);
+  }
+
+  /**
+   * Deletes all facet FilterNodes.
+   */
+  clearFacetFilterNodes () {
+    this.storage.delete(StorageKeys.FACET_FILTER_NODES);
+  }
+
+  /**
+   * Deletes all FilterNodes in storage.
+   */
+  clearAllFilterNodes () {
+    this.storage.delete(StorageKeys.LOCATION_RADIUS_FILTER_NODE);
+    this.clearFacetFilterNodes();
+    this.storage.getAll().forEach((value, key) => {
+      if (key.startsWith(StorageKeys.STATIC_FILTER_NODES)) {
+        this.storage.delete(key);
+      }
+    });
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_filters_filtertype.js.html b/docs/core_filters_filtertype.js.html new file mode 100644 index 000000000..4ecd01b43 --- /dev/null +++ b/docs/core_filters_filtertype.js.html @@ -0,0 +1,65 @@ + + + + + JSDoc: Source: core/filters/filtertype.js + + + + + + + + + + +
+ +

Source: core/filters/filtertype.js

+ + + + + + +
+
+
/** @module FilterTypes */
+
+/**
+ * FilterType is an ENUM for the different types of filters in the SDK.
+ * @enum {string}
+ */
+const FilterType = {
+  STATIC: 'filter-type-static',
+  FACET: 'filter-type-facet',
+  RADIUS: 'filter-type-radius',
+  NLP: 'filter-type-nlp'
+};
+
+export default FilterType;
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_filters_matcher.js.html b/docs/core_filters_matcher.js.html new file mode 100644 index 000000000..b7a19165f --- /dev/null +++ b/docs/core_filters_matcher.js.html @@ -0,0 +1,64 @@ + + + + + JSDoc: Source: core/filters/matcher.js + + + + + + + + + + +
+ +

Source: core/filters/matcher.js

+ + + + + + +
+
+
/**
+ * A Matcher is a filtering operation for {@link Filter}s.
+ */
+const Matcher = {
+  Equals: '$eq',
+  NotEquals: '!$eq',
+  LessThan: '$lt',
+  LessThanOrEqualTo: '$le',
+  GreaterThan: '$gt',
+  GreaterThanOrEqualTo: '$ge',
+  Near: '$near'
+};
+export default Matcher;
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_filters_simplefilternode.js.html b/docs/core_filters_simplefilternode.js.html new file mode 100644 index 000000000..76eea8090 --- /dev/null +++ b/docs/core_filters_simplefilternode.js.html @@ -0,0 +1,153 @@ + + + + + JSDoc: Source: core/filters/simplefilternode.js + + + + + + + + + + +
+ +

Source: core/filters/simplefilternode.js

+ + + + + + +
+
+
/** @module SimpleFilterNode */
+
+import Filter from '../models/filter';
+import FilterMetadata from './filtermetadata';
+import FilterNode from './filternode';
+
+/**
+ * A SimpleFilterNode represents a single, atomic filter.
+ * An atomic filter is a filter that filters on a single field id,
+ * and does not contain any children filters.
+ */
+export default class SimpleFilterNode extends FilterNode {
+  constructor (filterNode = {}) {
+    super();
+    const { filter, metadata, remove } = filterNode;
+
+    /**
+     * The filter data.
+     * @type {Filter}
+     */
+    this.filter = Filter.from(filter);
+
+    /**
+     * Display metadata associated with the filter data.
+     * @type {FilterMetadata}
+     */
+    this.metadata = new FilterMetadata(metadata);
+
+    /**
+     * Remove callback function.
+     * @type {Function}
+     */
+    this._remove = remove || function () {};
+    Object.freeze(this);
+  }
+
+  /**
+   * Returns the filter associated with this node.
+   * @type {Filter}
+   */
+  getFilter () {
+    return this.filter;
+  }
+
+  /**
+   * Returns the children associated with this node (no children).
+   * @returns {Array<FilterNode>}
+   */
+  getChildren () {
+    return [];
+  }
+
+  /**
+   * Returns the filter metadata for this node's filter.
+   * @returns {FilterMetadata}
+   */
+  getMetadata () {
+    return this.metadata;
+  }
+
+  /**
+   * Recursively get all of the leaf SimpleFilterNodes.
+   * Since SimpleFilterNodes have no children this just returns itself.
+   * @returns {Array<SimpleFilterNode>}
+   */
+  getSimpleDescendants () {
+    return this;
+  }
+
+  /**
+   * Removes this filter node from the FilterRegistry.
+   */
+  remove () {
+    this._remove();
+  }
+
+  /**
+   * Returns whether this SimpleFilterNode's filter is equal to another SimpleFilterNode's
+   * @param {SimpleFilterNode} node
+   * @returns {boolean}
+   */
+  hasSameFilterAs (otherNode) {
+    const thisFilter = this.getFilter();
+    const otherFilter = otherNode.getFilter();
+    const thisFieldId = thisFilter.getFilterKey();
+    const otherFieldId = otherFilter.getFilterKey();
+    if (thisFieldId !== otherFieldId) {
+      return false;
+    }
+    const thisMatchersToValues = thisFilter[thisFieldId];
+    const otherMatchersToValues = otherFilter[otherFieldId];
+    const thisMatchers = Object.keys(thisMatchersToValues);
+    const otherMatchers = Object.keys(otherMatchersToValues);
+    if (thisMatchers.length !== otherMatchers.length) {
+      return false;
+    }
+    return thisMatchers.every(m =>
+      otherMatchersToValues.hasOwnProperty(m) &&
+      otherMatchersToValues[m] === thisMatchersToValues[m]
+    );
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_http_apirequest.js.html b/docs/core_http_apirequest.js.html index 7f302d01c..c2f072e47 100644 --- a/docs/core_http_apirequest.js.html +++ b/docs/core_http_apirequest.js.html @@ -29,14 +29,20 @@

Source: core/http/apirequest.js

/** @module ApiRequest */
 
 import HttpRequester from './httprequester';
-import { API_BASE_URL } from '../constants';
+import { LIB_VERSION, PRODUCTION } from '../constants';
+import SearchParams from '../../ui/dom/searchparams'; // TODO ideally this would be passed in as a param
+import { AnswersBasicError } from '../errors/errors';
+import StorageKeys from '../storage/storagekeys';
+import { getLiveApiUrl } from '../utils/urlutils';
 
 /**
  * ApiRequest is the base class for all API requests.
  * It defines all of the core properties required to make a request
  */
 export default class ApiRequest {
-  constructor (opts = {}) {
+  // TODO (tmeyer): Create an ApiService interface and pass an implementation to the current
+  // consumers of ApiRequest as a dependency.
+  constructor (opts = {}, storage) {
     /**
      * An abstraction used for making network request and handling errors
      * @type {HttpRequester}
@@ -44,12 +50,19 @@ 

Source: core/http/apirequest.js

*/ this._requester = new HttpRequester(); + /** + * The environment the request should be made to + * @type {string} + * @private + */ + this._environment = opts.environment || PRODUCTION; + /** * The baseUrl to use for making a request * @type {string} * @private */ - this._baseUrl = opts.baseUrl || API_BASE_URL; + this._baseUrl = opts.baseUrl || getLiveApiUrl(this._environment); /** * The endpoint to use in the url (appended to the {baseUrl}) @@ -78,32 +91,63 @@

Source: core/http/apirequest.js

* @private */ this._params = opts.params || {}; + + if (!storage) { + throw new AnswersBasicError('Must include storage', 'ApiRequest'); + } + /** + * @type {Storage} + * @private + */ + this._storage = storage; } /** * get creates a new `GET` request to the server using the configuration of the request class - * @returns {Promise} + * + * @param {Object} opts Any configuration options to use for the GET request. + * @returns {Promise<Response>} */ - get () { - return this._requester.get(this._baseUrl + this._endpoint, this.params(this._params)); + get (opts) { + return this._requester.get( + this._baseUrl + this._endpoint, + Object.assign({}, this.baseParams(), this.sanitizeParams(this._params)), + opts + ); } + /** + * @param {Object} opts + * @returns {Promise<Response>} + */ post (opts) { - return this._requester.post(this._baseUrl + this._endpoint, this.params(this._params), opts); + return this._requester.post( + this._baseUrl + this._endpoint, + this.baseParams() /* urlParams */, + this.sanitizeParams(this._params) /* jsonBody */, + opts /* requestConfig */); } - params (params) { - var baseParams = { + /** + * @returns {Object} + * @private + */ + baseParams () { + let baseParams = { 'v': this._version, - 'api_key': this._apiKey + 'api_key': this._apiKey, + 'jsLibVersion': LIB_VERSION, + 'sessionTrackingEnabled': this._storage.get(StorageKeys.SESSIONS_OPT_IN).value }; - - const urlParams = new URL(window.location.toString()).searchParams; - + const urlParams = new SearchParams(this._storage.getCurrentStateUrlMerged()); if (urlParams.has('beta')) { baseParams['beta'] = urlParams.get('beta'); } + return baseParams; + } + + sanitizeParams (params = {}) { // Remove any paramaters whos value is `undefined`. // // NOTE(billy) Probably better to be explicit about how to handle this at the request building level, @@ -115,7 +159,7 @@

Source: core/http/apirequest.js

} }); - return Object.assign(baseParams, params || {}); + return params; } }
@@ -128,13 +172,13 @@

Source: core/http/apirequest.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/core_http_httprequester.js.html b/docs/core_http_httprequester.js.html index 27f1ee24c..31e2889b7 100644 --- a/docs/core_http_httprequester.js.html +++ b/docs/core_http_httprequester.js.html @@ -28,7 +28,9 @@

Source: core/http/httprequester.js

/** @module HttpRequester */
 
-/* global fetch */
+/* global fetch, XMLHttpRequest, ActiveXObject */
+
+import { fetch as fetchPolyfill } from 'cross-fetch';
 
 /**
  * Types of HTTP requests
@@ -59,28 +61,83 @@ 

Source: core/http/httprequester.js

/** * Create a POST HTTP request * @param {string} url The url to make a request to - * @param {Object} data The data to provide (gets encoded into the URL) - * @param {Object} opts Configuration options to use for the request + * @param {Object} urlParams The params to encode into the URL + * @param {Object} jsonBody The request body (json) to provide with the POST request + * @param {Object} requestConfig Configuration options to use for the request */ - post (url, data, opts) { + post (url, urlParams, jsonBody, requestConfig) { return this.request( Methods.POST, - url, - Object.assign({ - body: JSON.stringify(data), + this.encodeParams(url, urlParams), + Object.assign({}, { + body: JSON.stringify(jsonBody), credentials: undefined - }, opts) + }, requestConfig) ); } request (method, url, opts) { - return fetch(url, Object.assign({ - method, - credentials: 'include' - }, opts)); + const reqArgs = Object.assign({}, { + 'method': method, + 'credentials': 'include' + }, opts); + + return this._fetch(url, reqArgs); + } + + // TODO (agrow) investigate removing this + // Use imported fetchPolyfill if it does not already exist on window + _fetch (url, reqArgs) { + if (!window.fetch) { + return fetchPolyfill(url, reqArgs); + } + return fetch(url, reqArgs); + } + + /** + * Send a beacon to the provided url which will send a non-blocking request + * to the server that is guaranteed to send before page load. No response is returned, + * so beacons are primarily used for analytics reporting. + * @param {string} url The url to send the beacon to + * @param {object} data The data payload to send in the beacon + * @return {boolean} true if the request is successfully queued + */ + beacon (url, data) { + return this._sendBeacon(url, JSON.stringify(data)); + } + + // TODO (agrow) investigate removing this + // Navigator.sendBeacon polyfill + // Combination of the compact Financial Times polyfill: + // https://github.com/Financial-Times/polyfill-library/blob/master/polyfills/navigator/sendBeacon/polyfill.js + // with the async-by-default behavior of Miguel Mota's polyfill: + // https://github.com/miguelmota/Navigator.sendBeacon/blob/master/sendbeacon.js + _sendBeacon (url, data) { + if (window.navigator && window.navigator.sendBeacon) { + return window.navigator.sendBeacon(url, data); + } + + var event = window.event && window.event.type; + var sync = event === 'unload' || event === 'beforeunload'; + var xhr = ('XMLHttpRequest' in window) ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP'); + xhr.open('POST', url, !sync); + xhr.setRequestHeader('Accept', '*/*'); + if (typeof data === 'string') { + xhr.setRequestHeader('Content-Type', 'text/plain;charset=UTF-8'); + } else if (Object.prototype.toString.call(data) === '[object Blob]') { + if (data.type) { + xhr.setRequestHeader('Content-Type', data.type); + } + } + xhr.send(data); + return true; } encodeParams (url, params) { + if (typeof params !== 'object') { + return; + } + let hasParam = url.indexOf('?') > -1; let searchQuery = ''; @@ -107,13 +164,13 @@

Source: core/http/httprequester.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/core_i18n_translationprocessor.js.html b/docs/core_i18n_translationprocessor.js.html new file mode 100644 index 000000000..7a77c5863 --- /dev/null +++ b/docs/core_i18n_translationprocessor.js.html @@ -0,0 +1,111 @@ + + + + + JSDoc: Source: core/i18n/translationprocessor.js + + + + + + + + + + +
+ +

Source: core/i18n/translationprocessor.js

+ + + + + + +
+
+
import { getNPlurals, getPluralFunc, hasLang } from 'plural-forms/dist/minimal-safe';
+
+export default class TranslationProcessor {
+  /**
+   * Processes a translation which includes performing interpolation, pluralization, or
+   * both
+   * @param {string | Object} translations The translation, or an object containing
+   * translated plural forms
+   * @param {Object} interpolationParams Params to use during interpolation
+   * @param {number} count The count associated with the pluralization
+   * @param {string} language The langauge associated with the pluralization
+   * @param {string} escapeExpression A function which escapes HTML in the passed string
+   * @returns {string} The translation with any interpolation or pluralization applied
+   */
+  static process (translations, interpolationParams, count, language, escapeExpression) {
+    const stringToInterpolate = (typeof translations === 'string')
+      ? translations
+      : this._selectPluralForm(translations, count, language);
+
+    return this._interpolate(stringToInterpolate, interpolationParams, escapeExpression);
+  }
+
+  /**
+   * Returns the correct plural form given a translations object and count.
+   * @param {Object} translations
+   * @param {number} count
+   * @param {string} language
+   * @returns {string}
+   */
+  static _selectPluralForm (translations, count, language) {
+    if (!hasLang(language)) {
+      language = 'en';
+    }
+    const oneToNArray = this._generateArrayOneToN(language);
+    const pluralFormIndex = getPluralFunc(language)(count, oneToNArray);
+    return translations[pluralFormIndex];
+  }
+
+  /**
+   * @param {string} language
+   * @returns {Array} an array of the form [0, 1, 2, ..., nPluralForms]
+   */
+  static _generateArrayOneToN (language) {
+    const numberOfPluralForms = getNPlurals(language);
+    return Array.from((new Array(numberOfPluralForms)).keys());
+  }
+
+  static _interpolate (stringToInterpolate, interpolationParams, escapeExpression) {
+    if (interpolationParams && !escapeExpression) {
+      throw new Error('An escapeExpression function must be provided when processing translations with interpolation');
+    }
+
+    const interpolationRegex = /\[\[([a-zA-Z0-9]+)\]\]/g;
+
+    return stringToInterpolate.replace(interpolationRegex, (match, interpolationKey) => {
+      const interpolation = interpolationParams[interpolationKey];
+      return escapeExpression(interpolation);
+    });
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_index.js.html b/docs/core_index.js.html index 3c1bc5c68..47b6e12dd 100644 --- a/docs/core_index.js.html +++ b/docs/core_index.js.html @@ -28,8 +28,8 @@

Source: core/index.js

/** @module */
 
-export { default as SearchApi } from './search/searchapi';
 export { default as AnalyticsReporter } from './analytics/analyticsreporter';
+export { default as NoopAnalyticsReporter } from './analytics/noopanalyticsreporter';
 export { default as ModuleData } from './storage/moduledata';
 export { default as Storage } from './storage/storage';
 
@@ -42,13 +42,13 @@

Source: core/index.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/core_models_alternativevertical.js.html b/docs/core_models_alternativevertical.js.html new file mode 100644 index 000000000..8bdb485f6 --- /dev/null +++ b/docs/core_models_alternativevertical.js.html @@ -0,0 +1,110 @@ + + + + + JSDoc: Source: core/models/alternativevertical.js + + + + + + + + + + +
+ +

Source: core/models/alternativevertical.js

+ + + + + + +
+
+
import { AnswersConfigError } from '../errors/errors';
+
+/**
+ * The AlternativeVertical is a model that is used to power the search
+ * suggestions info box. It's initialized through the configuration provided
+ * to the component.
+ */
+export default class AlternativeVertical {
+  constructor (config) {
+    /**
+     * The name of the vertical that is exposed for the link
+     * @type {string}
+     */
+    this.label = config.label;
+    if (typeof this.label !== 'string') {
+      throw new AnswersConfigError(
+        'label is a required configuration option for verticalPage.',
+        'AlternativeVertical'
+      );
+    }
+
+    /**
+     * The complete URL, including the params
+     * @type {string}
+     */
+    this.url = config.url;
+    if (typeof this.url !== 'string') {
+      throw new AnswersConfigError(
+        'url is a required configuration option for verticalPage.',
+        'AlternativeVertical'
+      );
+    }
+
+    /**
+     * name of an icon from the default icon set
+     * @type {string}
+     */
+    this.iconName = config.iconName;
+
+    /**
+     * URL of an icon
+     * @type {string}
+     */
+    this.iconUrl = config.iconUrl;
+
+    /**
+     * Whether the vertical has an icon
+     * @type {string}
+     */
+    this.hasIcon = this.iconName || this.iconUrl;
+
+    /**
+     * The number of results to display next to each alternative
+     * vertical
+     * @type {number}
+     */
+    this.resultsCount = config.resultsCount;
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_models_alternativeverticals.js.html b/docs/core_models_alternativeverticals.js.html new file mode 100644 index 000000000..c05a3ea18 --- /dev/null +++ b/docs/core_models_alternativeverticals.js.html @@ -0,0 +1,80 @@ + + + + + JSDoc: Source: core/models/alternativeverticals.js + + + + + + + + + + +
+ +

Source: core/models/alternativeverticals.js

+ + + + + + +
+
+
/** @module AlternativeVerticals */
+
+import VerticalResults from './verticalresults';
+
+export default class AlternativeVerticals {
+  constructor (data) {
+    /**
+     * Alternative verticals that have results for the current query
+     * @type {Section}
+     */
+    this.alternativeVerticals = data || [];
+  }
+
+  /**
+   * Create alternative verticals from server data
+   *
+   * @param {Object[]} alternativeVerticals
+   * @param {Object<string, function>} formatters applied to the result fields
+   */
+  static fromCore (alternativeVerticals, formatters) {
+    if (!alternativeVerticals || alternativeVerticals.length === 0) {
+      return new AlternativeVerticals();
+    }
+
+    return new AlternativeVerticals(alternativeVerticals.map(alternativeVertical => {
+      return VerticalResults.fromCore(alternativeVertical, {}, formatters);
+    }));
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_models_appliedhighlightedfields.js.html b/docs/core_models_appliedhighlightedfields.js.html new file mode 100644 index 000000000..08bb3c4a1 --- /dev/null +++ b/docs/core_models_appliedhighlightedfields.js.html @@ -0,0 +1,120 @@ + + + + + JSDoc: Source: core/models/appliedhighlightedfields.js + + + + + + + + + + +
+ +

Source: core/models/appliedhighlightedfields.js

+ + + + + + +
+
+
import HighlightedValue from './highlightedvalue';
+
+/**
+ * Represents highlighted fields with the highlighting applied
+ */
+export default class AppliedHighlightedFields {
+  /**
+   * Constructs a highlighted field map which consists of mappings from fields to highlighted
+   * value strings.
+   *
+   * @param {Object<string, string|object|array>} data Keyed by fieldName. It may consist of nested fields
+   *
+   * Example object:
+   *
+   * {
+   *   description: 'likes <strong>apple</strong> pie and green <strong>apple</strong>s',
+   *   c_favoriteFruits: [
+   *     {
+   *       apples: ['Granny Smith','Upton Pyne <strong>Apple</strong>'],
+   *       pears: ['Callery Pear']
+   *     }
+   *   ]
+   * }
+   */
+  constructor (data = {}) {
+    Object.assign(this, data);
+  }
+
+  /**
+   * Constructs an AppliedHighlightedFields object from an answers-core HighlightedField
+   *
+   * @param {import('@yext/answers-core').HighlightedFields} highlightedFields
+   * @returns {AppliedHighlightedFields}
+   */
+  static fromCore (highlightedFields) {
+    if (!highlightedFields || typeof highlightedFields !== 'object') {
+      return {};
+    }
+    const appliedHighlightedFields = this.computeHighlightedDataRecursively(highlightedFields);
+    return new AppliedHighlightedFields(appliedHighlightedFields);
+  }
+
+  /**
+   * Given an answers-core HighlightedFields tree, returns a new tree
+   * with highlighting applied to the leaf nodes.
+   *
+   * @param {import('@yext/answers-core').HighlightedFields} highlightedFields
+   * @returns {AppliedHighlightedFields}
+   */
+  static computeHighlightedDataRecursively (highlightedFields) {
+    if (this.isHighlightedFieldLeafNode(highlightedFields)) {
+      const { value, matchedSubstrings } = highlightedFields;
+      return new HighlightedValue().buildHighlightedValue(value, matchedSubstrings);
+    }
+    if (Array.isArray(highlightedFields)) {
+      return highlightedFields.map(
+        childFields => this.computeHighlightedDataRecursively(childFields));
+    }
+    return Object.entries(highlightedFields)
+      .reduce((computedFields, [fieldName, childFields]) => {
+        computedFields[fieldName] = this.computeHighlightedDataRecursively(childFields);
+        return computedFields;
+      }, {});
+  }
+
+  static isHighlightedFieldLeafNode ({ matchedSubstrings, value }) {
+    return matchedSubstrings !== undefined && value !== undefined;
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_models_appliedqueryfilter.js.html b/docs/core_models_appliedqueryfilter.js.html new file mode 100644 index 000000000..8fd992331 --- /dev/null +++ b/docs/core_models_appliedqueryfilter.js.html @@ -0,0 +1,83 @@ + + + + + JSDoc: Source: core/models/appliedqueryfilter.js + + + + + + + + + + +
+ +

Source: core/models/appliedqueryfilter.js

+ + + + + + +
+
+
import Filter from './filter';
+
+/**
+ * A model that represents a filter that the backend applied to a search
+ */
+export default class AppliedQueryFilter {
+  constructor (appliedQueryFilter = {}) {
+    this.key = appliedQueryFilter.key;
+    this.value = appliedQueryFilter.value;
+    this.filter = appliedQueryFilter.filter;
+    this.fieldId = appliedQueryFilter.fieldId;
+  }
+
+  /**
+   * Constructs an SDK AppliedQueryFilter from an answers-core AppliedQueryFilter
+   *
+   * @param {AppliedQueryFilter} appliedFilter from answers-core
+   * @returns {@link AppliedQueryFilter}
+   */
+  static fromCore (appliedFilter) {
+    if (!appliedFilter) {
+      return new AppliedQueryFilter();
+    }
+
+    return new AppliedQueryFilter({
+      key: appliedFilter.displayKey,
+      value: appliedFilter.displayValue,
+      filter: Filter.fromCoreSimpleFilter(appliedFilter.filter),
+      fieldId: appliedFilter.filter.fieldId
+    });
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_models_autocompletedata.js.html b/docs/core_models_autocompletedata.js.html index 28bcb5456..9ec69cb18 100644 --- a/docs/core_models_autocompletedata.js.html +++ b/docs/core_models_autocompletedata.js.html @@ -32,75 +32,21 @@

Source: core/models/autocompletedata.js

constructor (data = {}) { this.sections = data.sections || []; this.queryId = data.queryId || ''; + this.inputIntents = data.inputIntents || []; Object.freeze(this); } - - static from (response) { - let sections; - if (response.sections) { - sections = response.sections.map(s => ({ - label: s.label, - results: s.results.map(r => new AutoCompleteResult(r)) - })); - } else { - sections = [{ results: response.results.map(r => new AutoCompleteResult(r)) }]; - } - return new AutoCompleteData({ sections, queryId: response.queryId }); - } } export class AutoCompleteResult { constructor (data = {}) { this.filter = data.filter || {}; - this.highlightedValue = this.highlight(data); this.key = data.key || ''; this.matchedSubstrings = data.matchedSubstrings || []; this.value = data.value || ''; this.shortValue = data.shortValue || this.value; + this.intents = data.queryIntents || []; Object.freeze(this); } - - // TODO(jdelerme): consolidate with other highlight functions - highlight (data) { - const { value, shortValue, matchedSubstrings } = data; - const val = value || shortValue; - - if (!matchedSubstrings || matchedSubstrings.length === 0) { - return val; - } - - // Make sure our highlighted substrings are sorted - matchedSubstrings.sort((a, b) => { - if (a.offset < b.offset) { - return -1; - } - - if (a.offset > b.offset) { - return 1; - } - - return 0; - }); - - // Build our new value based on the highlights - let highlightedValue = ''; - let nextStart = 0; - - for (let j = 0; j < matchedSubstrings.length; j++) { - let start = Number(matchedSubstrings[j].offset); - let end = start + matchedSubstrings[j].length; - - highlightedValue += [val.slice(nextStart, start), '<strong>', val.slice(start, end), '</strong>'].join(''); - - if (j === matchedSubstrings.length - 1 && end < val.length) { - highlightedValue += val.slice(end); - } - - nextStart = end; - } - - return highlightedValue; - } }
@@ -112,13 +58,13 @@

Source: core/models/autocompletedata.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/core_models_directanswer.js.html b/docs/core_models_directanswer.js.html index 0ca6f65df..eafa89d96 100644 --- a/docs/core_models_directanswer.js.html +++ b/docs/core_models_directanswer.js.html @@ -33,6 +33,72 @@

Source: core/models/directanswer.js

Object.assign(this, directAnswer); Object.freeze(this); } + + /** + * Constructs an SDK DirectAnswer from an answers-core DirectAnswer and applies formatting + * + * @param {DirectAnswer} directAnswer from answers-core + * @param {Object<string, function>} formatters keyed by fieldApiName. If a formatter matches the fieldApiName + * of the direct answer, it will be applied to the direct answer value. + * @returns {DirectAnswer} + */ + static fromCore (directAnswer, formatters) { + if (!directAnswer) { + return new DirectAnswer(); + } + + const relatedResult = directAnswer.relatedResult || {}; + + const directAnswerData = { + answer: { + snippet: directAnswer.snippet, + entityName: directAnswer.entityName, + fieldName: directAnswer.fieldName, + fieldApiName: directAnswer.fieldApiName, + value: directAnswer.value, + fieldType: directAnswer.fieldType + }, + relatedItem: { + data: { + fieldValues: relatedResult.rawData, + id: relatedResult.id, + type: relatedResult.type, + website: relatedResult.link + }, + verticalConfigId: directAnswer.verticalKey + }, + type: directAnswer.type + }; + + const directAnswerFieldApiName = directAnswerData.answer.fieldApiName; + const formatterExistsForDirectAnswer = formatters && directAnswerFieldApiName in formatters; + + if (formatterExistsForDirectAnswer) { + const formattedValue = this._getFormattedValue(directAnswerData, formatters[directAnswerFieldApiName]); + directAnswerData.answer.value = formattedValue || directAnswerData.answer.value; + } + + return new DirectAnswer(directAnswerData); + } + + /** + * Applies a formatter to a direct answer value + * + * @param {Object} data directAnswerData + * @param {function} formatter a field formatter to apply to the answer value field + * @returns {string|null} the formatted value, or null if the formatter could not be applied + */ + static _getFormattedValue (data, formatter) { + if (!data.answer || !data.relatedItem) { + return null; + } + + return formatter( + data.answer.value, + data.relatedItem.data.fieldValues, + data.relatedItem.verticalConfigId, + true); + } }
@@ -44,13 +110,13 @@

Source: core/models/directanswer.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/core_models_dynamicfilters.js.html b/docs/core_models_dynamicfilters.js.html new file mode 100644 index 000000000..578e0c916 --- /dev/null +++ b/docs/core_models_dynamicfilters.js.html @@ -0,0 +1,87 @@ + + + + + JSDoc: Source: core/models/dynamicfilters.js + + + + + + + + + + +
+ +

Source: core/models/dynamicfilters.js

+ + + + + + +
+
+
/** @module DynamicFilters */
+
+import ResultsContext from '../storage/resultscontext';
+
+/**
+ * Model representing a set of dynamic filters
+ */
+export default class DynamicFilters {
+  constructor (data) {
+    /**
+     * The list of facets this model holds
+     * @type {DisplayableFacet[]} from answers-core
+     */
+    this.filters = data.filters || [];
+
+    /**
+     * The {@link ResultsContext} of the facets.
+     * @type {ResultsContext}
+     */
+    this.resultsContext = data.resultsContext;
+    Object.freeze(this);
+  }
+
+  /**
+   * Organize 'facets' from the answers-core into dynamic filters
+   * @param {DisplayableFacet[]} facets from answers-core
+   * @param {ResultsContext} resultsContext
+   * @returns {DynamicFilters}
+   */
+  static fromCore (facets = [], resultsContext = ResultsContext.NORMAL) {
+    return new DynamicFilters({
+      filters: facets,
+      resultsContext: resultsContext
+    });
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_models_facet.js.html b/docs/core_models_facet.js.html new file mode 100644 index 000000000..3605ec60d --- /dev/null +++ b/docs/core_models_facet.js.html @@ -0,0 +1,115 @@ + + + + + JSDoc: Source: core/models/facet.js + + + + + + + + + + +
+ +

Source: core/models/facet.js

+ + + + + + +
+
+
/** @module Facet */
+
+/**
+ * Model representing a facet filter with the format of
+ * {
+ *   "field_name": [ Filters... ],
+ *   ...
+ * }
+ *
+ * @see {@link Filter}
+ */
+export default class Facet {
+  constructor (data = {}) {
+    Object.assign(this, data);
+    Object.freeze(this);
+  }
+
+  /**
+   * Create a facet filter from a list of Filters
+   * @param {Array<string>} availableFieldIds array of expected field ids
+   * @param  {...Filter} filters The filters to use in this facet
+   * @returns {Facet}
+   */
+  static fromFilters (availableFieldIds, ...filters) {
+    const groups = {};
+    availableFieldIds.forEach(fieldId => {
+      groups[fieldId] = [];
+    });
+    const flatFilters = filters.flatMap(f => f.$or || f);
+    flatFilters.forEach(f => {
+      const key = f.getFilterKey();
+      if (!groups[key]) {
+        groups[key] = [];
+      }
+      groups[key].push(f);
+    });
+
+    return new Facet(groups);
+  }
+
+  /**
+   * Transforms an answers-core DisplayableFacet array into a Facet array
+   *
+   * @param {DisplayableFacet[]} coreFacets from answers-core
+   * @returns {Facet[]}
+   */
+  static fromCore (coreFacets = []) {
+    const facets = coreFacets.map(f => ({
+      label: f['displayName'],
+      fieldId: f['fieldId'],
+      options: f.options.map(o => ({
+        label: o['displayName'],
+        countLabel: o['count'],
+        selected: o['selected'],
+        filter: {
+          [f['fieldId']]: {
+            [o['matcher']]: o['value']
+          }
+        }
+      }))
+    })).map(f => new Facet(f));
+    return facets;
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_models_filter.js.html b/docs/core_models_filter.js.html index 30deb6bcc..411b7a940 100644 --- a/docs/core_models_filter.js.html +++ b/docs/core_models_filter.js.html @@ -28,6 +28,16 @@

Source: core/models/filter.js

/** @module Filter */
 
+import FilterCombinators from '../filters/filtercombinators';
+import Matcher from '../filters/matcher';
+
+const RANGE_MATCHERS = new Set([
+  Matcher.GreaterThan,
+  Matcher.GreaterThanOrEqualTo,
+  Matcher.LessThanOrEqualTo,
+  Matcher.LessThan
+]);
+
 /**
  * Represents an api filter and provides static methods for easily constructing Filters.
  * See https://developer.yext.com/docs/api-reference/#operation/listEntities for structure details
@@ -38,6 +48,64 @@ 

Source: core/models/filter.js

Object.freeze(this); } + /** + * A filter should have exactly ONE key. That key is EITHER the field name to filter by, or + * a special string such as $or or $and. + * @type {string} + */ + getFilterKey () { + if (Object.keys(this).length > 0) { + return Object.keys(this)[0]; + } + } + + /** + * Whether this filter is a range filter. + * + * @returns {boolean} + */ + isRangeFilter () { + const filterKey = this.getFilterKey(); + if (!filterKey) { + return false; + } + const matchers = Object.keys(this[filterKey]); + return matchers.every(m => RANGE_MATCHERS.has(m)); + } + + /** + * Create an empty filter + */ + static empty () { + return new Filter(); + } + + /** + * Wrap filter data in a Filter class + * @param {Object} filter + */ + static from (filter) { + return new Filter(filter); + } + + /** + * Constructs an SDK Filter model from an answers-core SimpleFilter model + * + * @param {SimpleFilter} filter from answers-core + * @returns {Filter} + */ + static fromCoreSimpleFilter (filter) { + if (!filter) { + return this.empty(); + } + + return new Filter({ + [filter.fieldId]: { + [filter.matcher]: filter.value + } + }); + } + /** * Parse a JSON format filter returned from the server into a Filter * @param {*} responseFilter A filter in JSON format returned from the backend @@ -54,7 +122,7 @@

Source: core/models/filter.js

*/ static or (...filters) { return new Filter({ - '$or': filters + [ FilterCombinators.OR ]: filters }); } @@ -65,31 +133,39 @@

Source: core/models/filter.js

*/ static and (...filters) { return new Filter({ - '$and': filters + [ FilterCombinators.AND ]: filters }); } /** - * OR filters with the same keys, then AND the resulting groups - * @param {...Filter} filters The filters to group + * Helper method for creating a range filter + * @param {string} field field id of the filter + * @param {number|string} min minimum value + * @param {number|string} max maximum value + * @param {boolean} isExclusive whether this is an inclusive or exclusive range * @returns {Filter} */ - static group (...filters) { - const groups = {}; - for (const filter of filters) { - const key = Object.keys(filter)[0]; - if (!groups[key]) { - groups[key] = []; - } - groups[key].push(filter); - } - - const groupFilters = []; - for (const field of Object.keys(groups)) { - groupFilters.push(groups[field].length > 1 ? Filter.or(...groups[field]) : groups[field][0]); + static range (field, min, max, isExclusive) { + const falsyMin = min === null || min === undefined || min === ''; + const falsyMax = max === null || max === undefined || max === ''; + if (falsyMin && falsyMax) { + return Filter.empty(); + } else if (falsyMax) { + return isExclusive + ? Filter.greaterThan(field, min) + : Filter.greaterThanEqual(field, min); + } else if (falsyMin) { + return isExclusive + ? Filter.lessThan(field, max) + : Filter.lessThanEqual(field, max); + } else if (min === max) { + return isExclusive + ? Filter.empty() + : Filter.equal(field, min); } - - return groupFilters.length > 1 ? Filter.and(...groupFilters) : groupFilters[0]; + return isExclusive + ? Filter.exclusiveRange(field, min, max) + : Filter.inclusiveRange(field, min, max); } /** @@ -99,7 +175,7 @@

Source: core/models/filter.js

* @returns {Filter} */ static equal (field, value) { - return Filter._fromMatcher(field, '$eq', value); + return Filter._fromMatcher(field, Matcher.Equals, value); } /** @@ -109,7 +185,7 @@

Source: core/models/filter.js

* @returns {Filter} */ static lessThan (field, value) { - return Filter._fromMatcher(field, '$lt', value); + return Filter._fromMatcher(field, Matcher.LessThan, value); } /** @@ -119,7 +195,7 @@

Source: core/models/filter.js

* @returns {Filter} */ static lessThanEqual (field, value) { - return Filter._fromMatcher(field, '$le', value); + return Filter._fromMatcher(field, Matcher.LessThanOrEqualTo, value); } /** @@ -129,7 +205,7 @@

Source: core/models/filter.js

* @returns {Filter} */ static greaterThan (field, value) { - return Filter._fromMatcher(field, '$gt', value); + return Filter._fromMatcher(field, Matcher.GreaterThan, value); } /** @@ -139,18 +215,49 @@

Source: core/models/filter.js

* @returns {Filter} */ static greaterThanEqual (field, value) { - return Filter._fromMatcher(field, '$ge', value); + return Filter._fromMatcher(field, Matcher.GreaterThanOrEqualTo, value); + } + + /** + * Create a new inclusive range filter + * @param {string} field The subject field of the filter + * @param {*} min The minimum value + * @param {*} max The maximum value + * @returns {Filter} + */ + static inclusiveRange (field, min, max) { + return new Filter({ + [field]: { + [Matcher.GreaterThanOrEqualTo]: min, + [Matcher.LessThanOrEqualTo]: max + } + }); } /** - * Create a new inclusive range filter for a field + * Create a new exclusive range filter * @param {string} field The subject field of the filter - * @param {*} min The minimum value of the range - * @param {*} max The maximum value of the ranger + * @param {*} min The minimum value + * @param {*} max The maximum value * @returns {Filter} */ - static range (field, min, max) { - return Filter.and(Filter.greaterThanEqual(field, min), Filter.lessThanEqual(field, max)); + static exclusiveRange (field, min, max) { + return new Filter({ + [field]: { + [Matcher.GreaterThan]: min, + [Matcher.LessThan]: max + } + }); + } + + /** + * Create a new position filter + * @param {number} lat The latitude of the position + * @param {number} lng The longitude of the position + * @param {number} radius The search radius (in meters) + */ + static position (lat, lng, radius) { + return Filter._fromMatcher('builtin.location', Matcher.Near, { lat, lng, radius }); } /** @@ -179,13 +286,13 @@

Source: core/models/filter.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/core_models_highlightedfields.js.html b/docs/core_models_highlightedfields.js.html new file mode 100644 index 000000000..226ad6dc3 --- /dev/null +++ b/docs/core_models_highlightedfields.js.html @@ -0,0 +1,96 @@ + + + + + JSDoc: Source: core/models/highlightedfields.js + + + + + + + + + + +
+ +

Source: core/models/highlightedfields.js

+ + + + + + +
+
+
/**
+ * Represents highlighted fields without the highlighting applied
+*/
+export default class HighlightedFields {
+  /**
+   * Constructs a highlighted fields object which consists of fields mapping to HighlightedValues
+   *
+   * @param {import('@yext/answers-core').HighlightedFields} highlightedFields
+   *
+   * Example object:
+   *
+   * {
+   *   description: {
+   *     value: 'likes apple pie and green apples',
+   *     matchedSubstrings: [
+   *       { offset: 6, length: 5 },
+   *       { offset: 26, length: 5 }
+   *     ]
+   *   },
+   *   c_favoriteFruits: [
+   *     {
+   *       apples: [
+   *         {
+   *           value: 'Granny Smith',
+   *           matchedSubstrings: []
+   *         },
+   *         {
+   *           value: 'Upton Pyne Apple',
+   *           matchedSubstrings: [{ offset: 11, length: 5}]
+   *         }
+   *       ],
+   *       pears: [
+   *         {
+   *           value: 'Callery Pear',
+   *           matchedSubstrings: []
+   *         }
+   *       ]
+   *     }
+   *   ]
+   * }
+   */
+  constructor (highlightedFields = {}) {
+    Object.assign(this, highlightedFields);
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_models_highlightedvalue.js.html b/docs/core_models_highlightedvalue.js.html new file mode 100644 index 000000000..b09e7175c --- /dev/null +++ b/docs/core_models_highlightedvalue.js.html @@ -0,0 +1,223 @@ + + + + + JSDoc: Source: core/models/highlightedvalue.js + + + + + + + + + + +
+ +

Source: core/models/highlightedvalue.js

+ + + + + + +
+
+
/** @module HighlightedValue */
+
+/**
+ * Model representing a highlighted value.
+ */
+export default class HighlightedValue {
+  constructor (data = {}) {
+    this.value = data.value || data.shortValue || '';
+    this.matchedSubstrings = data.matchedSubstrings || [];
+  }
+
+  /**
+   * get highlighted value string
+   * @returns {string}
+   */
+  get () {
+    this._sortMatchedSubstrings();
+    return this.buildHighlightedValue(this.value, this.matchedSubstrings);
+  }
+
+  /**
+   * get highlighted value string
+   * @param {Function} transformFunction takes a string and returns the transformed string
+   * @returns {string} The value interpolated with highlighting markup and transformed in between
+   */
+  getWithTransformFunction (transformFunction) {
+    this._sortMatchedSubstrings();
+    return this.buildHighlightedValue(this.value, this.matchedSubstrings, transformFunction);
+  }
+
+  /**
+   * get inverted highlighted value string
+   * @returns {string}
+   */
+  getInverted () {
+    this._sortMatchedSubstrings();
+    const invertedSubstrings = this._getInvertedSubstrings(this.matchedSubstrings, this.value.length);
+    return this.buildHighlightedValue(this.value, invertedSubstrings);
+  }
+
+  /**
+   * get inverted highlighted value string
+   * @param {Function} transformFunction takes a string and returns the transformed string
+   * @returns {string} The value interpolated with highlighting markup and transformed in between
+   */
+  getInvertedWithTransformFunction (transformFunction) {
+    this._sortMatchedSubstrings();
+    const invertedSubstrings = this._getInvertedSubstrings(this.matchedSubstrings, this.value.length);
+    return this.buildHighlightedValue(this.value, invertedSubstrings, transformFunction);
+  }
+
+  /**
+   * introduces highlighting to input data according to highlighting specifiers
+   *
+   * @param {Object} val input object to apply highlighting to
+   *
+   *  example object :
+   *  {
+   *    name: 'ATM',
+   *    featuredMessage: {
+   *      description: 'Save time & bank on your terms at over 1,800 ATMs'
+   *    }
+   *  }
+   *
+   * @param {Object} highlightedSubstrings highlighting specifiers to apply to input object
+   *
+   *  example object :
+   *  {
+   *    name: {
+   *      matchedSubstrings: [{
+   *        length: 3,
+   *        offset: 0
+   *      }],
+   *      value: 'ATM'
+   *    },
+   *    featuredMessage: {
+   *      description: {
+   *        matchedSubstrings: [{
+   *          length: 4,
+   *          offset: 45
+   *        }],
+   *        value: 'Save time & bank on your terms at over 1,800 ATMs'
+   *      }
+   *    }
+   *  }
+   *
+   * @param {Function} transformFunction function to apply to strings in between highlighting markup
+   *
+   *  example function :
+   *  function (string) {
+   *    return handlebars.escapeExpression(string);
+   *  }
+   *
+   * @returns {string} copy of input value with highlighting applied
+   *
+   *  example object :
+   *  {
+   *    name: '<strong>ATM</strong>',
+   *    featuredMessage: {
+   *      description: 'Save time & bank on your terms at over 1,800 <strong>ATMs</strong>'
+   *    }
+   *  }
+   *
+   */
+  buildHighlightedValue (
+    val,
+    highlightedSubstrings,
+    transformFunction = function (x) { return x; }
+  ) {
+    let highlightedValue = '';
+    let nextStart = 0;
+
+    if (highlightedSubstrings.length === 0) {
+      return transformFunction(val);
+    }
+
+    for (let j = 0; j < highlightedSubstrings.length; j++) {
+      let start = Number(highlightedSubstrings[j].offset);
+      let end = start + highlightedSubstrings[j].length;
+
+      highlightedValue += [
+        transformFunction(val.slice(nextStart, start)),
+        '<strong>',
+        transformFunction(val.slice(start, end)),
+        '</strong>'
+      ].join('');
+
+      if (j === highlightedSubstrings.length - 1 && end < val.length) {
+        highlightedValue += transformFunction(val.slice(end));
+      }
+
+      nextStart = end;
+    }
+
+    return highlightedValue;
+  }
+
+  _sortMatchedSubstrings () {
+    this.matchedSubstrings.sort((a, b) => {
+      if (a.offset < b.offset) {
+        return -1;
+      }
+
+      if (a.offset > b.offset) {
+        return 1;
+      }
+
+      return 0;
+    });
+  }
+
+  _getInvertedSubstrings (matchedSubstrings, valueLength) {
+    const invertedSubstrings = [];
+    for (let i = 0; i < matchedSubstrings.length; i++) {
+      const substring = matchedSubstrings[i];
+      const nextOffset = substring.offset + substring.length;
+      if (i === 0 && substring.offset !== 0) {
+        invertedSubstrings.push({ offset: 0, length: substring.offset });
+      }
+
+      if (valueLength > nextOffset) {
+        invertedSubstrings.push({
+          offset: nextOffset,
+          length: i < matchedSubstrings.length - 1
+            ? matchedSubstrings[i + 1].offset - nextOffset
+            : valueLength - nextOffset
+        });
+      }
+    }
+    return invertedSubstrings;
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_models_locationbias.js.html b/docs/core_models_locationbias.js.html new file mode 100644 index 000000000..3cf716c9f --- /dev/null +++ b/docs/core_models_locationbias.js.html @@ -0,0 +1,121 @@ + + + + + JSDoc: Source: core/models/locationbias.js + + + + + + + + + + +
+ +

Source: core/models/locationbias.js

+ + + + + + +
+
+
/** @module LocationBias */
+
+import SearchStates from '../storage/searchstates';
+
+/**
+ * LocationBias is the core state model
+ * to power the LocationBias component
+ */
+export default class LocationBias {
+  constructor (data) {
+    /**
+     * The location bias accuracy which are IP, DEVICE and UNKNWON
+     * @type {string}
+     */
+    this.accuracy = data.accuracy || null;
+
+    /**
+     * The latitude used for location bias
+     * @type {number}
+     */
+    this.latitude = data.latitude || null;
+
+    /**
+     * The longitude used for location bias
+     * @type {number}
+     */
+    this.longitude = data.longitude || null;
+
+    /**
+     * The location display name
+     * @type {string}
+     */
+    this.locationDisplayName = data.locationDisplayName || null;
+
+    /**
+     * Whether the search is loading or completed
+     */
+    this.searchState = data.searchState;
+  }
+
+  /**
+   * Construct a LocationBias object representing loading results
+   * @return {LocationBias}
+   */
+  static searchLoading () {
+    return new LocationBias({ searchState: SearchStates.SEARCH_LOADING });
+  }
+
+  /*
+  * Constructs an SDK LocationBias model from an answers-core LocationBias
+  *
+  * @param {LocationBias} locationBias from answers-core
+  * @returns {LocationBias}
+  */
+  static fromCore (locationBias) {
+    if (!locationBias) {
+      return new LocationBias({
+        accuracy: 'UNKNOWN'
+      });
+    }
+
+    return new LocationBias({
+      accuracy: locationBias.method,
+      latitude: locationBias.latitude,
+      longitude: locationBias.longitude,
+      locationDisplayName: locationBias.displayName,
+      searchState: SearchStates.SEARCH_COMPLETE
+    });
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_models_navigation.js.html b/docs/core_models_navigation.js.html index 4d6a05af2..d0f681fc0 100644 --- a/docs/core_models_navigation.js.html +++ b/docs/core_models_navigation.js.html @@ -44,6 +44,16 @@

Source: core/models/navigation.js

} return new Navigation(nav); } + + /** + * Constructs a Navigation model from an answers-core VerticalResults array + * + * @param {VerticalResults[]} verticalResults + */ + static fromCore (verticalResults) { + const verticalKeys = verticalResults.map(result => result.verticalKey); + return new Navigation(verticalKeys); + } }
@@ -55,13 +65,13 @@

Source: core/models/navigation.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/core_models_querytriggers.js.html b/docs/core_models_querytriggers.js.html new file mode 100644 index 000000000..44ae794f9 --- /dev/null +++ b/docs/core_models_querytriggers.js.html @@ -0,0 +1,68 @@ + + + + + JSDoc: Source: core/models/querytriggers.js + + + + + + + + + + +
+ +

Source: core/models/querytriggers.js

+ + + + + + +
+
+
/** @module QueryTriggers */
+
+/**
+ * QueryTriggers is an ENUM of the possible triggers for a
+ * query update.
+ *
+ * @enum {string}
+ */
+const QueryTriggers = {
+  INITIALIZE: 'initialize',
+  QUERY_PARAMETER: 'query-parameter',
+  SUGGEST: 'suggest',
+  FILTER_COMPONENT: 'filter-component',
+  PAGINATION: 'pagination',
+  SEARCH_BAR: 'search-bar'
+};
+export default QueryTriggers;
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_models_questionsubmission.js.html b/docs/core_models_questionsubmission.js.html new file mode 100644 index 000000000..aa5257aa2 --- /dev/null +++ b/docs/core_models_questionsubmission.js.html @@ -0,0 +1,119 @@ + + + + + JSDoc: Source: core/models/questionsubmission.js + + + + + + + + + + +
+ +

Source: core/models/questionsubmission.js

+ + + + + + +
+
+
/** @module QuestionSubmission */
+
+/**
+ * QuestionSubmission is the core state model
+ * to power the QuestionSubmission component
+ */
+export default class QuestionSubmission {
+  constructor (question = {}, errors) {
+    /**
+     * The author of the question
+     * @type {string}
+     */
+    this.name = question.name || null;
+
+    /**
+     * The email address of the question
+     * @type {string}
+     */
+    this.email = question.email || null;
+
+    /**
+     * True if the privacy policy was approved
+     * @type {boolean}
+     */
+    this.privacyPolicy = question.privacyPolicy || null;
+
+    /**
+     * The question to be sent to the server
+     * @type {string}
+     */
+    this.questionText = question.questionText || null;
+
+    /**
+     * Alternative question meta information
+     * @type {string}
+     */
+    this.questionDescription = question.questionDescription || null;
+
+    /**
+     * Whether the form is expanded or not. Defaults to true.
+     */
+    this.questionExpanded = typeof question.expanded !== 'boolean' || question.expanded;
+
+    /**
+     * Contains any errors about the question submission
+     * @type {object}
+     */
+    this.errors = errors || null;
+
+    /**
+     * Whether the form has been submitted or not. Defaults to false.
+     */
+    this.questionSubmitted = question.submitted || false;
+
+    Object.freeze(this);
+  }
+
+  static submitted () {
+    return {
+      questionSubmitted: true,
+      questionExpanded: true
+    };
+  }
+
+  static errors (question, errors) {
+    return QuestionSubmission(question, errors);
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_models_result.js.html b/docs/core_models_result.js.html index 0897ed965..6fc187686 100644 --- a/docs/core_models_result.js.html +++ b/docs/core_models_result.js.html @@ -28,25 +28,207 @@

Source: core/models/result.js

/** @module Result */
 
+import AppliedHighlightedFields from './appliedhighlightedfields';
+import HighlightedFields from './highlightedfields';
+import { truncate } from '../utils/strings';
+import { AnswersCoreError } from '../errors/errors';
+
 export default class Result {
   constructor (data = {}) {
-    Object.assign(this, data);
+    /**
+     * The raw profile data
+     * @type {Object}
+     * @private
+     */
+    this._raw = data.raw || null;
+
+    /**
+     * The formatted profile data
+     * @type {Object}
+     * @private
+     */
+    this._formatted = data.formatted;
+
+    /**
+     * The highlighted profile data with highlights applied to applicable fields
+     * @type {Object}
+     * @private
+     */
+    this._highlighted = data.highlighted;
+
+    /**
+     * An object that lists the substrings to highlight for each applicable field.
+     * @type {Object}
+     */
+    this.highlightedFields = data.highlightedFields;
+
+    /**
+     * The index number of the result
+     * @type {Number}
+     */
+    this.ordinal = data.ordinal || null;
+
+    /**
+     * The title of the result card
+     * @type {string|null}
+     */
+    this.title = data.title || null;
+
+    /**
+     * The body of the details section of the result card, can contain HTML
+     * @type {string| null}
+     */
+    this.details = data.details || null;
+
+    /**
+     * The destination link for the title of the result card
+     * @type {string|null}
+     */
+    this.link = data.link || null;
+
+    /**
+     * The Entity ID, or other unique identifier, used for to power interactivity
+     * @type {string|null}
+     */
+    this.id = data.id || null;
+
+    /**
+     * The subtitle on the result card
+     * @type {string|null}
+     */
+    this.subtitle = data.subtitle || null;
+
+    /**
+     * The class modifier, usually derived from the vertical configuration ID
+     * Used to apply different styling to different result card types
+     * @type {string|null}
+     */
+    this.modifier = data.modifier || null;
+
+    /**
+     * A large date, of the format { month: 'Jan', day: '01' }
+     * @type {Object|null}
+     */
+    this.bigDate = data.bigDate || null;
+
+    /**
+     * An image profile object, expected to have a url property
+     * @type {Object|null}
+     */
+    this.image = data.image || null;
+
+    /**
+     * An array of calls to action, of the format:
+     * { icon: '', url: '', text: '', eventType: '', eventOptions: {}}
+     * @type {Array}
+     */
+    this.callsToAction = data.callsToAction || [];
+
+    /**
+     * Determines if an accordian result should be collapsed by default
+     * @type {boolean}
+     */
+    this.collapsed = data.collapsed === undefined ? true : data.collapsed;
+
+    /**
+     * @type {number}
+     */
+    this.distance = data.distance || null;
+
+    /**
+     * @type {number}
+     */
+    this.distanceFromFilter = data.distanceFromFilter || null;
   }
 
   /**
-   * resultsData expected format: { data: { ... }, highlightedFields: { ... }}
+   * Constructs an SDK Result from an answers-core Result
+   *
+   * @param {Result} result from answers-core
+   * @param {Object<string, function>} formatters applied to the result fields
+   * @param {string} verticalKey the verticalKey associated with the result
+   * @returns {@link Result}
    */
-  static from (resultsData) {
-    let results = [];
-    for (let i = 0; i < resultsData.length; i++) {
-      // TODO use resultData.highlightedFields to
-      // transform resultData.data into html-friendly strings that highlight values.
-
-      // Check for new data format, otherwise fallback to legacy
-      results.push(new Result(resultsData[i].data || resultsData[i]));
+  static fromCore (result, formatters, verticalKey) {
+    const highlightedFields = new HighlightedFields(result.highlightedFields);
+    const appliedHighlightedFields = AppliedHighlightedFields.fromCore(result.highlightedFields);
+    const details = appliedHighlightedFields.description || result.description;
+    const truncatedDetails = truncate(details);
+
+    const resultData = {
+      raw: result.rawData,
+      ordinal: result.index,
+      title: result.name,
+      details: truncatedDetails,
+      link: result.link,
+      id: result.id,
+      distance: result.distance,
+      distanceFromFilter: result.distanceFromFilter,
+      highlighted: appliedHighlightedFields,
+      highlightedFields
+    };
+
+    if (result.source !== 'KNOWLEDGE_MANAGER') {
+      return new Result(resultData);
+    }
+
+    const formattedData = this._getFormattedData(resultData, formatters, verticalKey);
+    resultData.formatted = formattedData;
+
+    if (formattedData.description !== undefined) {
+      resultData.details = formattedData.description;
     }
 
-    return results;
+    return new Result(resultData);
+  }
+
+  /**
+   * Returns an object which contains formatted fields
+   *
+   * @param {Object} resultData the same shape as the input to the Result constructor
+   * @param {Object<string, function>} formatters to apply to the result fields
+   * @param {string} verticalKey the verticalKey associated with the result
+   * @returns {Object<string, string>} keys are field names and values are the formatted data
+   */
+  static _getFormattedData (resultData, formatters, verticalKey) {
+    const formattedData = {};
+
+    if (!formatters || !resultData.raw) {
+      return formattedData;
+    }
+
+    if (Object.keys(formatters).length === 0) {
+      return formattedData;
+    }
+
+    Object.entries(resultData.raw).forEach(([fieldName, fieldVal]) => {
+      // check if a field formatter exists for the current entity profile field
+      if (formatters[fieldName] === undefined) {
+        return;
+      }
+      // verify the field formatter provided is a formatter function as expected
+      if (typeof formatters[fieldName] !== 'function') {
+        throw new AnswersCoreError('Field formatter is not of expected type function', 'Result');
+      }
+
+      // if highlighted version of field value is available, make it available to field formatter
+      let highlightedFieldVal = null;
+      if (resultData.highlighted && resultData.highlighted[fieldName]) {
+        highlightedFieldVal = resultData.highlighted[fieldName];
+      }
+
+      // call formatter function associated with the field name
+      // the input object defines the interface that field formatter functions work with
+      formattedData[fieldName] = formatters[fieldName]({
+        entityProfileData: resultData.raw,
+        entityFieldValue: fieldVal,
+        highlightedEntityFieldValue: highlightedFieldVal,
+        verticalId: verticalKey,
+        isDirectAnswer: false
+      });
+    });
+
+    return formattedData;
   }
 }
 
@@ -59,13 +241,13 @@

Source: core/models/result.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/core_models_searchconfig.js.html b/docs/core_models_searchconfig.js.html new file mode 100644 index 000000000..8b5c715fa --- /dev/null +++ b/docs/core_models_searchconfig.js.html @@ -0,0 +1,86 @@ + + + + + JSDoc: Source: core/models/searchconfig.js + + + + + + + + + + +
+ +

Source: core/models/searchconfig.js

+ + + + + + +
+
+
import { AnswersConfigError } from '../errors/errors';
+
+/** @module SearchConfig */
+
+export default class SearchConfig {
+  constructor (config = {}) {
+    /**
+     * The max results per search.
+     * Also defines the number of results per page, if pagination is enabled
+     * @type {number}
+     */
+    this.limit = config.limit || 20;
+
+    /**
+     * The vertical key to use for all searches
+     * @type {string}
+     */
+    this.verticalKey = config.verticalKey || null;
+
+    /**
+     * A default search to use on initialization when the user hasn't provided a query
+     * @type {string}
+     */
+    this.defaultInitialSearch = config.defaultInitialSearch;
+
+    this.validate();
+    Object.freeze(this);
+  }
+
+  validate () {
+    if (typeof this.limit !== 'number' || this.limit < 1 || this.limit > 50) {
+      throw new AnswersConfigError('Search Limit must be between 1 and 50', 'SearchConfig');
+    }
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_models_section.js.html b/docs/core_models_section.js.html index 0590cefca..720adee1e 100644 --- a/docs/core_models_section.js.html +++ b/docs/core_models_section.js.html @@ -28,28 +28,33 @@

Source: core/models/section.js

/** @module Section */
 
-import Result from './result';
+import SearchStates from '../storage/searchstates';
 
 export default class Section {
-  constructor (data, url) {
+  constructor (data = {}, url, resultsContext) {
+    this.searchState = SearchStates.SEARCH_COMPLETE;
     this.verticalConfigId = data.verticalConfigId || null;
     this.resultsCount = data.resultsCount || 0;
     this.encodedState = data.encodedState || '';
-    this.appliedQueryFilters = AppliedQueryFilter.from(data.appliedQueryFilters);
+    this.appliedQueryFilters = data.appliedQueryFilters;
     this.facets = data.facets || null;
-    this.results = Result.from(data.results);
+    this.results = data.results;
     this.map = Section.parseMap(data.results);
     this.verticalURL = url || null;
+    this.resultsContext = resultsContext;
   }
 
   static parseMap (results) {
+    if (!results) {
+      return {};
+    }
+
     let mapMarkers = [];
 
     let centerCoordinates = {};
 
     for (let j = 0; j < results.length; j++) {
-      // TODO(billy) Remove legacy fallback from all data format
-      let result = results[j].data || results[j];
+      let result = results[j]._raw;
       if (result && result.yextDisplayCoordinate) {
         if (!centerCoordinates.latitude) {
           centerCoordinates = {
@@ -71,46 +76,6 @@ 

Source: core/models/section.js

'mapMarkers': mapMarkers }; } - - static from (modules, urls) { - let sections = []; - if (!modules) { - return sections; - } - - if (!Array.isArray(modules)) { - return new Section(modules); - } - - // Our sections should contain a property of mapMarker objects - for (let i = 0; i < modules.length; i++) { - sections.push( - new Section( - modules[i], - urls[modules[i].verticalConfigId] - ) - ); - } - - return sections; - } -} - -class AppliedQueryFilter { - // Support legacy model and new model until fully migrated. - // TODO(billy) Remove the left expression during assignment when migrated. - constructor (appliedQueryFilter) { - this.key = appliedQueryFilter.key || appliedQueryFilter.displayKey; - this.value = appliedQueryFilter.value || appliedQueryFilter.displayValue; - } - - static from (appliedQueryFilters) { - let filters = []; - for (let i = 0; i < appliedQueryFilters.length; i++) { - filters.push(new AppliedQueryFilter(appliedQueryFilters[i])); - } - return filters; - } }
@@ -122,13 +87,13 @@

Source: core/models/section.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/core_models_spellcheck.js.html b/docs/core_models_spellcheck.js.html new file mode 100644 index 000000000..a4bf9a5ba --- /dev/null +++ b/docs/core_models_spellcheck.js.html @@ -0,0 +1,103 @@ + + + + + JSDoc: Source: core/models/spellcheck.js + + + + + + + + + + +
+ +

Source: core/models/spellcheck.js

+ + + + + + +
+
+
/** @module SpellCheck */
+
+/**
+ * SpellCheck is the core state model
+ * to power the SpellCheck component
+ */
+export default class SpellCheck {
+  constructor (data) {
+    /**
+     * The original query
+     * @type {string}
+     */
+    this.query = data.query || null;
+
+    /**
+     * The corrected query
+     * @type {string}
+     */
+    this.correctedQuery = data.correctedQuery || null;
+
+    /**
+     * The spell check type
+     * @type {string}
+     */
+    this.type = data.type || null;
+
+    /**
+     * Should show spell check or not
+     * @type {boolean}
+     */
+    this.shouldShow = this.correctedQuery !== null;
+  }
+
+  /**
+   * Create a spell check model from the provided data
+   *
+   * @param {Object} response The spell check response
+   */
+  static fromCore (response) {
+    if (!response) {
+      return {};
+    }
+
+    return new SpellCheck({
+      query: response.originalQuery,
+      correctedQuery: {
+        value: response.correctedQuery
+      },
+      type: response.type
+    });
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_models_universalresults.js.html b/docs/core_models_universalresults.js.html index 4e0bc7da1..b492333f0 100644 --- a/docs/core_models_universalresults.js.html +++ b/docs/core_models_universalresults.js.html @@ -28,18 +28,48 @@

Source: core/models/universalresults.js

/** @module UniversalResults */
 
-import Section from './section';
+import SearchStates from '../storage/searchstates';
+import VerticalResults from './verticalresults';
 
 export default class UniversalResults {
   constructor (data) {
     this.queryId = data.queryId || null;
     this.sections = data.sections || [];
+
+    /**
+     * The current state of the search, used to render different templates before, during,
+     * and after loading
+     * @type {SearchState}
+     */
+    this.searchState = data.searchState || SearchStates.SEARCH_COMPLETE;
+  }
+
+  /**
+   * Construct a UniversalResults object representing loading results
+   * @return {UniversalResults}
+   */
+  static searchLoading () {
+    return new UniversalResults({ searchState: SearchStates.SEARCH_LOADING });
   }
 
-  static from (response, urls) {
+  /**
+   * Constructs an SDK UniversalResults model from an answers-core UniversalSearchResponse
+   *
+   * @param {UniversalSearchResponse} response from answers-core
+   * @param {Object<string, string>} urls keyed by vertical key
+   * @param {Object<string, function>} formatters applied to the result fields
+   * @returns {@link UniversalResults}
+   */
+  static fromCore (response, urls, formatters) {
+    if (!response) {
+      return new UniversalResults();
+    }
+
     return new UniversalResults({
       queryId: response.queryId,
-      sections: Section.from(response.modules, urls)
+      sections: response.verticalResults.map(verticalResults => {
+        return VerticalResults.fromCore(verticalResults, urls, formatters);
+      })
     });
   }
 }
@@ -53,13 +83,13 @@ 

Source: core/models/universalresults.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/core_models_verticalpagesconfig.js.html b/docs/core_models_verticalpagesconfig.js.html new file mode 100644 index 000000000..63dcc0120 --- /dev/null +++ b/docs/core_models_verticalpagesconfig.js.html @@ -0,0 +1,129 @@ + + + + + JSDoc: Source: core/models/verticalpagesconfig.js + + + + + + + + + + +
+ +

Source: core/models/verticalpagesconfig.js

+ + + + + + +
+
+
/** @module VerticalPagesConfig */
+
+export class VerticalPageConfig {
+  constructor (config = {}) {
+    /**
+     * The name of the tab that is exposed for the link
+     * @type {string}
+     */
+    this.label = config.label || null;
+
+    /**
+     * The complete URL, including the params
+     * @type {string}
+     */
+    this.url = config.url || null;
+
+    /**
+     * The serverside vertical config id that this is referenced to.
+     * By providing this, enables dynamic sorting based on results.
+     * @type {string}
+     */
+    this.verticalKey = config.verticalKey || null;
+
+    /**
+     * Determines whether to show this tab in the navigation component
+     * @type {boolean}
+     */
+    this.hideInNavigation = config.hideInNavigation || false;
+
+    /**
+     * Determines whether to show this tab first in the order
+     * @type {boolean}
+     */
+    this.isFirst = config.isFirst || false;
+
+    /**
+     * Determines whether or not to apply a special class to the
+     * markup to determine if it's an active tab
+     * @type {boolean}
+     */
+    this.isActive = config.isActive || false;
+
+    /**
+     * URL of an icon
+     * @type {string}
+     */
+    this.iconUrl = config.iconUrl;
+
+    /**
+     * name of an icon from the default icon set
+     * @type {string}
+     */
+    this.icon = config.icon;
+    Object.freeze(this);
+  }
+
+  validate () {
+  }
+}
+
+export default class VerticalPagesConfig {
+  constructor (pages = []) {
+    this.verticalPagesConfig = VerticalPagesConfig.from(pages);
+  }
+
+  /**
+   * Using a getter that copies the data instead of providing a reference prevents it from being mutated.
+   * This is important for global configuration.
+   * @returns {Array<VerticalPageConfig>}
+   */
+  get () {
+    return this.verticalPagesConfig.map(page => ({ ...page }));
+  }
+
+  static from (pages) {
+    return pages.map(page => new VerticalPageConfig(page));
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_models_verticalresults.js.html b/docs/core_models_verticalresults.js.html index c773cf4a7..d33598b99 100644 --- a/docs/core_models_verticalresults.js.html +++ b/docs/core_models_verticalresults.js.html @@ -28,16 +28,79 @@

Source: core/models/verticalresults.js

/** @module VerticalResults */
 
+import { AnswersCoreError } from '../errors/errors';
 import Section from './section';
+import SearchStates from '../storage/searchstates';
+import AppliedQueryFilter from './appliedqueryfilter';
+import Result from './result';
+import ResultsContext from '../storage/resultscontext';
 
 export default class VerticalResults {
   constructor (data = {}) {
-    Object.assign(this, data);
+    Object.assign(this, { searchState: SearchStates.SEARCH_COMPLETE }, data);
+
+    /**
+     * The context of the results, used to provide more information about why
+     * these specific results were returned.
+     * @type {ResultsContext}
+     */
+    this.resultsContext = data.resultsContext;
+
     Object.freeze(this);
   }
 
-  static from (response) {
-    return new VerticalResults(Section.from(response));
+  /**
+   * Append the provided results to the current results
+   * @param {VerticalResults} results the results to append to the current results
+   */
+  append (results) {
+    if (results.resultsContext !== this.resultsContext) {
+      throw new AnswersCoreError('Cannot merge results with different contexts', 'VerticalResults');
+    }
+    const merged = { ...this };
+    merged.resultsContext = this.resultsContext;
+    merged.results = this.results.concat(results.results);
+    merged.map.mapMarkers = this.map.mapMarkers.concat(results.map.mapMarkers);
+    return new VerticalResults(merged);
+  }
+
+  /**
+   * Constructs an SDK Section model from an answers-core VerticalResult
+   *
+   * @param {VerticalResults} verticalResults
+   * @param {Object<string, string>} urls keyed by vertical key
+   * @param {Object<string, function>} formatters applied to the result fields
+   * @param {ResultsContext} resultsContext
+   * @param {string} verticalKeyFromRequest
+   * @returns {@link Section}
+   */
+  static fromCore (verticalResults, urls = {}, formatters, resultsContext = ResultsContext.NORMAL, verticalKeyFromRequest) {
+    if (!verticalResults) {
+      return new Section();
+    }
+
+    const verticalKey = verticalResults.verticalKey || verticalKeyFromRequest;
+
+    return new Section(
+      {
+        verticalConfigId: verticalKey,
+        resultsCount: verticalResults.resultsCount,
+        appliedQueryFilters: verticalResults.appliedQueryFilters.map(AppliedQueryFilter.fromCore),
+        results: verticalResults.results.map(result => {
+          return Result.fromCore(result, formatters, verticalKey);
+        })
+      },
+      urls[verticalKey],
+      resultsContext
+    );
+  }
+
+  /**
+   * Construct a VerticalResults object representing loading results
+   * @return {VerticalResults}
+   */
+  static searchLoading () {
+    return new VerticalResults({ searchState: SearchStates.SEARCH_LOADING });
   }
 
   static areDuplicateNamesAllowed () {
@@ -54,13 +117,13 @@ 

Source: core/models/verticalresults.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/core_search_autocomplete.js.html b/docs/core_search_autocomplete.js.html deleted file mode 100644 index 54a0dd67b..000000000 --- a/docs/core_search_autocomplete.js.html +++ /dev/null @@ -1,153 +0,0 @@ - - - - - JSDoc: Source: core/search/autocomplete.js - - - - - - - - - - -
- -

Source: core/search/autocomplete.js

- - - - - - -
-
-
/** @module AutoComplete */
-
-import ApiRequest from '../http/apirequest';
-import AutoCompleteDataTransformer from './autocompletedatatransformer';
-
-/**
- * A wrapper around the AutoComplete {ApiRequest} endpoints
- */
-export default class AutoComplete {
-  constructor (opts = {}) {
-    let params = new URL(window.location.toString()).searchParams;
-    let isLocal = params.get('local');
-
-    /**
-     * The baseUrl to use for making a request
-     * @type {string}
-     * @private
-     */
-    this._baseUrl = isLocal ? 'http://' + window.location.hostname : 'https://liveapi.yext.com';
-
-    /**
-     * The API Key to use for the request
-     * @type {string}
-     * @private
-     */
-    this._apiKey = opts.apiKey || null;
-
-    /**
-     * The Answers Key to use for the request
-     * @type {string}
-     * @private
-     */
-    this._answersKey = opts.answersKey || null;
-
-    /**
-     * The version of the API to make a request to
-     * @type {string}
-     * @private
-     */
-    this._version = opts.version || 20190101 || 20190301;
-  }
-
-  /**
-   * Autocomplete filters
-   * @param {string} input The input to use for auto complete
-   */
-  queryFilter (input, verticalKey, barKey) {
-    let request = new ApiRequest({
-      baseUrl: this._baseUrl,
-      endpoint: '/v2/accounts/me/answers/filtersearch',
-      apiKey: this._apiKey,
-      version: this._version,
-      params: {
-        'input': input,
-        'answersKey': this._answersKey,
-        'experiencekey': verticalKey,
-        'inputKey': barKey
-      }
-    });
-
-    return request.get()
-      .then(response => response.json())
-      .then(response => AutoCompleteDataTransformer.filter(response.response, barKey))
-      .catch(error => console.error(error));
-  }
-
-  queryVertical (input, verticalKey, barKey) {
-    let request = new ApiRequest({
-      baseUrl: this._baseUrl,
-      endpoint: '/v2/accounts/me/entities/autocomplete',
-      apiKey: this._apiKey,
-      version: this._version,
-      params: {
-        'input': input,
-        'experienceKey': verticalKey,
-        'barKey': barKey
-      }
-    });
-
-    return request.get()
-      .then(response => response.json())
-      .then(response => AutoCompleteDataTransformer.vertical(response.response, request._params.barKey))
-      .catch(error => console.error(error));
-  }
-
-  queryUniversal (queryString) {
-    let request = new ApiRequest({
-      baseUrl: this._baseUrl,
-      endpoint: '/v2/accounts/me/answers/autocomplete',
-      apiKey: this._apiKey,
-      version: this._version,
-      params: {
-        'input': queryString,
-        'answersKey': this._answersKey
-      }
-    });
-
-    return request.get(queryString)
-      .then(response => response.json())
-      .then(response => AutoCompleteDataTransformer.universal(response.response));
-  }
-}
-
-
-
- - - - -
- - - -
- -
- Documentation generated by JSDoc 3.6.2 on Wed Jul 03 2019 10:34:41 GMT-0400 (Eastern Daylight Time) -
- - - - - diff --git a/docs/core_search_autocompleteapi.js.html b/docs/core_search_autocompleteapi.js.html deleted file mode 100644 index 6a9641a14..000000000 --- a/docs/core_search_autocompleteapi.js.html +++ /dev/null @@ -1,169 +0,0 @@ - - - - - JSDoc: Source: core/search/autocompleteapi.js - - - - - - - - - - -
- -

Source: core/search/autocompleteapi.js

- - - - - - -
-
-
/** @module AutoCompleteApi */
-
-import ApiRequest from '../http/apirequest';
-import AutoCompleteDataTransformer from './autocompletedatatransformer';
-import { AnswersBasicError, AnswersEndpointError } from '../errors/errors';
-
-/**
- * AutoCompleteApi exposes an interface for network related matters
- * for all the autocomplete endpoints.
- */
-export default class AutoCompleteApi {
-  constructor (opts = {}) {
-    /**
-     * The API Key to use for the request
-     * @type {string}
-     * @private
-     */
-    if (!opts.apiKey) {
-      throw new AnswersBasicError('Api Key is required', 'AutoComplete');
-    }
-    this._apiKey = opts.apiKey;
-
-    /**
-     * The Answers Key to use for the request
-     * @type {string}
-     * @private
-     */
-    if (!opts.answersKey) {
-      throw new AnswersBasicError('Answers Key is required', 'AutoComplete');
-    }
-    this._answersKey = opts.answersKey;
-
-    /**
-     * The version of the API to make a request to
-     * @type {string}
-     * @private
-     */
-    this._version = opts.version || 20190101 || 20190301;
-
-    /**
-     * The locale to use for the request
-     * @type {string}
-     * @private
-     */
-    if (!opts.locale) {
-      throw new AnswersBasicError('Locale is required', 'AutoComplete');
-    }
-    this._locale = opts.locale;
-  }
-
-  /**
-   * Autocomplete filters
-   * @param {string} input The input to use for auto complete
-   */
-  queryFilter (input, verticalKey, barKey) {
-    let request = new ApiRequest({
-      endpoint: '/v2/accounts/me/answers/filtersearch',
-      apiKey: this._apiKey,
-      version: this._version,
-      params: {
-        'input': input,
-        'answersKey': this._answersKey,
-        'experienceKey': verticalKey,
-        'inputKey': barKey,
-        'locale': this._locale
-      }
-    });
-
-    return request.get()
-      .then(response => response.json())
-      .then(response => AutoCompleteDataTransformer.filter(response.response, barKey))
-      .catch(error => {
-        throw new AnswersEndpointError('Filter search request failed', 'AutoComplete', error);
-      });
-  }
-
-  queryVertical (input, verticalKey, barKey) {
-    let request = new ApiRequest({
-      endpoint: '/v2/accounts/me/answers/autocomplete',
-      apiKey: this._apiKey,
-      version: this._version,
-      params: {
-        'input': input,
-        'answersKey': this._answersKey,
-        'experienceKey': verticalKey,
-        'barKey': barKey,
-        'locale': this._locale
-      }
-    });
-
-    return request.get()
-      .then(response => response.json())
-      .then(response => AutoCompleteDataTransformer.vertical(response.response, request._params.barKey))
-      .catch(error => {
-        throw new AnswersEndpointError('Vertical search request failed', 'AutoComplete', error);
-      });
-  }
-
-  queryUniversal (queryString) {
-    let request = new ApiRequest({
-      endpoint: '/v2/accounts/me/answers/autocomplete',
-      apiKey: this._apiKey,
-      version: this._version,
-      params: {
-        'input': queryString,
-        'answersKey': this._answersKey,
-        'locale': this._locale
-      }
-    });
-
-    return request.get(queryString)
-      .then(response => response.json())
-      .then(response => AutoCompleteDataTransformer.universal(response.response))
-      .catch(error => {
-        throw new AnswersEndpointError('Universal search request failed', 'AutoComplete', error);
-      });
-  }
-}
-
-
-
- - - - -
- - - -
- -
- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) -
- - - - - diff --git a/docs/core_search_autocompletedatatransformer.js.html b/docs/core_search_autocompletedatatransformer.js.html deleted file mode 100644 index ce3bec62b..000000000 --- a/docs/core_search_autocompletedatatransformer.js.html +++ /dev/null @@ -1,89 +0,0 @@ - - - - - JSDoc: Source: core/search/autocompletedatatransformer.js - - - - - - - - - - -
- -

Source: core/search/autocompletedatatransformer.js

- - - - - - -
-
-
/** @module AutoCompleteDataTransformer */
-
-import AutoCompleteData from '../models/autocompletedata';
-
-/**
- * A Data Transformer that takes the response object from a AutoComplete request
- * And transforms in to a front-end oriented data structure that our
- * component library and core storage understand.
- *
- * TODO(billy) Create our own front-end data models
- */
-export default class AutoCompleteDataTransformer {
-  static clean (moduleId, data) {
-    if (data.sections && data.sections.length === 0) {
-      delete data.sections;
-    }
-
-    if (data.sections && data.sections.length === 1 && data.sections[0].results.length === 0) {
-      delete data.sections;
-    }
-
-    return {
-      [moduleId]: data
-    };
-  }
-
-  static universal (response) {
-    return AutoCompleteData.from(response);
-  }
-
-  static filter (response) {
-    return AutoCompleteData.from(response);
-  }
-
-  static vertical (response) {
-    return AutoCompleteData.from(response);
-  }
-}
-
-
-
- - - - -
- - - -
- -
- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) -
- - - - - diff --git a/docs/core_search_autocompleteresponsetransformer.js.html b/docs/core_search_autocompleteresponsetransformer.js.html new file mode 100644 index 000000000..670b401b4 --- /dev/null +++ b/docs/core_search_autocompleteresponsetransformer.js.html @@ -0,0 +1,125 @@ + + + + + JSDoc: Source: core/search/autocompleteresponsetransformer.js + + + + + + + + + + +
+ +

Source: core/search/autocompleteresponsetransformer.js

+ + + + + + +
+
+
import AutoCompleteData, { AutoCompleteResult } from '../models/autocompletedata';
+
+/**
+ * A data transformer that takes the response object from an
+ * AutoComplete request and transforms it into a front-end oriented
+ * data structure that our component library and core storage understand.
+ */
+export default class AutoCompleteResponseTransformer {
+  /**
+   * Converts a universal or vertical autocomplete response from the
+   * core library into an object that the SDK understands.
+   *
+   * @param {import('@yext/answers-core').AutocompleteResponse} response
+   *  the response passed from the core library
+   * @returns {AutoCompleteData}
+   */
+  static transformAutoCompleteResponse (response) {
+    const sections = [{
+      results: response.results.map(result => this._transformAutoCompleteResult(result)),
+      resultsCount: response.results.length
+    }];
+    return new AutoCompleteData({
+      sections: sections,
+      queryId: response.queryId,
+      inputIntents: response.inputIntents
+    });
+  }
+
+  /**
+   * Converts a filter search response from the
+   * core library into an object that the SDK understands.
+   *
+   * @param {import('@yext/answers-core').FilterSearchResponse} response
+   *  the response passed from the core library
+   * @returns {AutoCompleteData}
+   */
+  static transformFilterSearchResponse (response) {
+    if (response.sectioned && response.sections) {
+      const transformedSections = response.sections.map(section => ({
+        label: section.label,
+        results: section.results.map(result => this._transformAutoCompleteResult(result)),
+        resultsCount: section.results.length
+      }));
+      return new AutoCompleteData({
+        sections: transformedSections,
+        queryId: response.queryId,
+        inputIntents: response.inputIntents
+      });
+    } else {
+      return this.transformAutoCompleteResponse(response);
+    }
+  }
+
+  static _transformAutoCompleteResult (result) {
+    const transformedFilter = result.filter ? this._transformFilter(result.filter) : {};
+    return new AutoCompleteResult({
+      filter: transformedFilter,
+      key: result.key,
+      matchedSubstrings: result.matchedSubstrings,
+      value: result.value
+    });
+  }
+
+  static _transformFilter (filter) {
+    const fieldId = filter.fieldId;
+    const matcher = filter.matcher;
+    const value = filter.value;
+    return {
+      [fieldId]: {
+        [matcher]: value
+      }
+    };
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_search_search.js.html b/docs/core_search_search.js.html deleted file mode 100644 index 3a6f2965c..000000000 --- a/docs/core_search_search.js.html +++ /dev/null @@ -1,126 +0,0 @@ - - - - - JSDoc: Source: core/search/search.js - - - - - - - - - - -
- -

Source: core/search/search.js

- - - - - - -
-
-
/** @module Search */
-
-import HttpRequester from '../http/httprequester';
-import ApiRequest from '../http/apirequest';
-
-export default class Search {
-  constructor (opts = {}) {
-    let params = new URL(window.location.toString()).searchParams;
-    let isLocal = params.get('local');
-
-    this._requester = new HttpRequester();
-
-    /**
-     * The baseUrl to use for making a request
-     * @type {string}
-     * @private
-     */
-    this._baseUrl = isLocal ? 'http://' + window.location.hostname : 'https://liveapi.yext.com';
-
-    /**
-     * A local reference to the API Key to use for the request
-     * @type {string}
-     * @private
-     */
-    this._apiKey = opts.apiKey || null;
-
-    /**
-     * A local reference to the Answers Key to use for the request
-     * @type {string}
-     * @private
-     */
-    this._answersKey = opts.answersKey || null;
-
-    /**
-     * The version of the API to make a request to
-     * @type {string}
-     * @private
-     */
-    this._version = opts.version || 20190101 || 20190301;
-  }
-
-  verticalQuery (queryString, verticalKey, filter) {
-    let request = new ApiRequest({
-      baseUrl: this._baseUrl,
-      endpoint: '/v2/accounts/me/answers/vertical/query',
-      apiKey: this._apiKey,
-      version: this._version,
-      params: {
-        'input': queryString,
-        'answersKey': this._answersKey,
-        'filters': filter,
-        'verticalKey': verticalKey
-      }
-    });
-
-    return request.get()
-      .then(response => response.json());
-  }
-
-  query (queryString) {
-    let request = new ApiRequest({
-      baseUrl: this._baseUrl,
-      endpoint: '/v2/accounts/me/answers/query',
-      apiKey: this._apiKey,
-      version: this._version,
-      params: {
-        'input': queryString,
-        'answersKey': this._answersKey
-      }
-    });
-
-    return request.get()
-      .then(response => response.json());
-  }
-}
-
-
-
- - - - -
- - - -
- -
- Documentation generated by JSDoc 3.6.2 on Wed Jul 03 2019 10:34:41 GMT-0400 (Eastern Daylight Time) -
- - - - - diff --git a/docs/core_search_searchapi.js.html b/docs/core_search_searchapi.js.html deleted file mode 100644 index a75a6fd29..000000000 --- a/docs/core_search_searchapi.js.html +++ /dev/null @@ -1,134 +0,0 @@ - - - - - JSDoc: Source: core/search/searchapi.js - - - - - - - - - - -
- -

Source: core/search/searchapi.js

- - - - - - -
-
-
/** @module SearchApi */
-
-import ApiRequest from '../http/apirequest';
-import { AnswersBasicError } from '../errors/errors';
-
-/**
- * SearchApi is the API for doing various types of search
- * over the network (e.g. vertical or universal)
- */
-export default class SearchApi {
-  constructor (opts = {}) {
-    /**
-     * A local reference to the API Key to use for the request
-     * @type {string}
-     * @private
-     */
-    if (!opts.apiKey) {
-      throw new AnswersBasicError('Api Key is required', 'Search');
-    }
-    this._apiKey = opts.apiKey;
-
-    /**
-     * A local reference to the Answers Key to use for the request
-     * @type {string}
-     * @private
-     */
-    if (!opts.answersKey) {
-      throw new AnswersBasicError('Answers Key is required', 'Search');
-    }
-    this._answersKey = opts.answersKey;
-
-    /**
-     * The version of the API to make a request to
-     * @type {string}
-     * @private
-     */
-    this._version = opts.version || 20190101 || 20190301;
-
-    /**
-     * A local reference to the locale to use for the request
-     * @type {string}
-     * @private
-     */
-    if (!opts.locale) {
-      throw new AnswersBasicError('Locale is required', 'Search');
-    }
-    this._locale = opts.locale;
-  }
-
-  verticalQuery (queryString, verticalKey, filter) {
-    let request = new ApiRequest({
-      endpoint: '/v2/accounts/me/answers/vertical/query',
-      apiKey: this._apiKey,
-      version: this._version,
-      params: {
-        'input': queryString,
-        'answersKey': this._answersKey,
-        'filters': filter,
-        'verticalKey': verticalKey,
-        'locale': this._locale
-      }
-    });
-
-    return request.get()
-      .then(response => response.json());
-  }
-
-  query (queryString) {
-    let request = new ApiRequest({
-      endpoint: '/v2/accounts/me/answers/query',
-      apiKey: this._apiKey,
-      version: this._version,
-      params: {
-        'input': queryString,
-        'answersKey': this._answersKey,
-        'locale': this._locale
-      }
-    });
-
-    return request.get()
-      .then(response => response.json());
-  }
-}
-
-
-
- - - - -
- - - -
- -
- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) -
- - - - - diff --git a/docs/core_search_searchdatatransformer.js.html b/docs/core_search_searchdatatransformer.js.html index 419fbab48..3efb7875f 100644 --- a/docs/core_search_searchdatatransformer.js.html +++ b/docs/core_search_searchdatatransformer.js.html @@ -32,7 +32,12 @@

Source: core/search/searchdatatransformer.js

import DirectAnswer from '../models/directanswer'; import Navigation from '../models/navigation'; import VerticalResults from '../models/verticalresults'; +import SpellCheck from '../models/spellcheck'; import StorageKeys from '../storage/storagekeys'; +import DynamicFilters from '../models/dynamicfilters'; +import LocationBias from '../models/locationbias'; +import AlternativeVerticals from '../models/alternativeverticals'; +import ResultsContext from '../storage/resultscontext'; /** * A Data Transformer that takes the response object from a Search request @@ -40,21 +45,56 @@

Source: core/search/searchdatatransformer.js

* component library and core storage understand. */ export default class SearchDataTransformer { - static transform (data, urls = {}) { - let response = data.response; + static transformUniversal (data, urls = {}, formatters) { return { - queryId: response.queryId, - [StorageKeys.NAVIGATION]: Navigation.from(response.modules), - [StorageKeys.DIRECT_ANSWER]: new DirectAnswer(response.directAnswer), - [StorageKeys.UNIVERSAL_RESULTS]: UniversalResults.from(response, urls) + [StorageKeys.QUERY_ID]: data.queryId, + [StorageKeys.NAVIGATION]: Navigation.fromCore(data.verticalResults), + [StorageKeys.DIRECT_ANSWER]: DirectAnswer.fromCore(data.directAnswer, formatters), + [StorageKeys.UNIVERSAL_RESULTS]: UniversalResults.fromCore(data, urls, formatters), + [StorageKeys.SPELL_CHECK]: SpellCheck.fromCore(data.spellCheck), + [StorageKeys.LOCATION_BIAS]: LocationBias.fromCore(data.locationBias) }; } - static transformVertical (data) { + static transformVertical (coreResponse, formatters, verticalKey) { + const hasResults = coreResponse.verticalResults && + coreResponse.verticalResults.results && + coreResponse.verticalResults.resultsCount > 0; + + let resultsContext = ResultsContext.NORMAL; + let response = coreResponse; + if (!hasResults) { + resultsContext = ResultsContext.NO_RESULTS; + response = SearchDataTransformer._reshapeForNoResults(coreResponse); + } + + return { + [StorageKeys.QUERY_ID]: response.queryId, + [StorageKeys.NAVIGATION]: new Navigation(), // Vertical doesn't respond with ordering, so use empty nav. + [StorageKeys.VERTICAL_RESULTS]: VerticalResults.fromCore( + response.verticalResults, {}, formatters, resultsContext, verticalKey), + [StorageKeys.DYNAMIC_FILTERS]: DynamicFilters.fromCore(response.facets, resultsContext), + [StorageKeys.SPELL_CHECK]: SpellCheck.fromCore(response.spellCheck), + [StorageKeys.ALTERNATIVE_VERTICALS]: AlternativeVerticals.fromCore(response.alternativeVerticals, formatters), + [StorageKeys.LOCATION_BIAS]: LocationBias.fromCore(response.locationBias) + }; + } + + /** + * Form response as if the results from `allResultsForVertical` were the actual + * results in `results` + * + * @param {Object} response The server response + */ + static _reshapeForNoResults (response) { + const allResultsForVertical = response.allResultsForVertical || {}; + const { results, resultsCount } = allResultsForVertical.verticalResults || {}; return { - queryId: data.response.queryId, - [StorageKeys.NAVIGATION]: new Navigation(), // Veritcal doesn't respond with ordering, so use empty nav. - [StorageKeys.VERTICAL_RESULTS]: VerticalResults.from(data.response) + ...response, + results: results || [], + resultsCount: resultsCount || 0, + verticalResults: allResultsForVertical.verticalResults, + facets: allResultsForVertical.facets }; } } @@ -68,13 +108,13 @@

Source: core/search/searchdatatransformer.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/core_search_searchoptionsfactory.js.html b/docs/core_search_searchoptionsfactory.js.html new file mode 100644 index 000000000..7b5b9a671 --- /dev/null +++ b/docs/core_search_searchoptionsfactory.js.html @@ -0,0 +1,99 @@ + + + + + JSDoc: Source: core/search/searchoptionsfactory.js + + + + + + + + + + +
+ +

Source: core/search/searchoptionsfactory.js

+ + + + + + +
+
+
import QueryTriggers from '../models/querytriggers';
+
+/**
+ * SearchOptionsFactory is responsible for determining what search options to use
+ * for a given QUERY_TRIGGER.
+ */
+export default class SearchOptionsFactory {
+  /**
+   * Given a QUERY_TRIGGER, return the search options for the given trigger.
+   *
+   * @returns {Object}
+   */
+  create (queryTrigger) {
+    switch (queryTrigger) {
+      case QueryTriggers.FILTER_COMPONENT:
+        return {
+          setQueryParams: true,
+          resetPagination: true,
+          useFacets: true
+        };
+      case QueryTriggers.PAGINATION:
+        return {
+          setQueryParams: true,
+          resetPagination: false,
+          useFacets: true,
+          sendQueryId: true
+        };
+      case QueryTriggers.QUERY_PARAMETER:
+      case QueryTriggers.INITIALIZE:
+        return {
+          setQueryParams: true,
+          resetPagination: false,
+          useFacets: true
+        };
+      case QueryTriggers.SUGGEST:
+        return {
+          setQueryParams: true,
+          resetPagination: true,
+          useFacets: true
+        };
+      default:
+        return {
+          setQueryParams: true,
+          resetPagination: true
+        };
+    }
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_services_analyticsreporterservice.js.html b/docs/core_services_analyticsreporterservice.js.html new file mode 100644 index 000000000..9bcb059e5 --- /dev/null +++ b/docs/core_services_analyticsreporterservice.js.html @@ -0,0 +1,73 @@ + + + + + JSDoc: Source: core/services/analyticsreporterservice.js + + + + + + + + + + +
+ +

Source: core/services/analyticsreporterservice.js

+ + + + + + +
+
+
/** @typedef {import('../analytics/analyticsevent').default} AnalyticsEvent */
+
+/**
+ * AnslyticsReporterService exposes an interface for reporting analytics events
+ * to a backend
+ *
+ * @interface
+ */
+export default class AnalyticsReporterService {
+  /**
+   * Report an analytics event
+   * @param {AnalyticsEvent} event
+   * @returns {boolean} whether the event was successfully reported
+   */
+  report (event) {}
+
+  /**
+   * Enable or disable conversion tracking
+   * @param {boolean} isEnabled
+   */
+  setConversionTrackingEnabled (isEnabled) {}
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_services_errorreporterservice.js.html b/docs/core_services_errorreporterservice.js.html new file mode 100644 index 000000000..cf0981bc4 --- /dev/null +++ b/docs/core_services_errorreporterservice.js.html @@ -0,0 +1,66 @@ + + + + + JSDoc: Source: core/services/errorreporterservice.js + + + + + + + + + + +
+ +

Source: core/services/errorreporterservice.js

+ + + + + + +
+
+
/** @typedef {import('../errors/errors').AnswersBaseError} AnswersBaseError */
+
+/**
+ * ErrorReporterService exposes an interface for reporting errors to the console
+ * and to a backend
+ *
+ * @interface
+ */
+export default class ErrorReporterService {
+  /**
+   * Reports an error to backend servers for logging
+   * @param {AnswersBaseError} err The error to be reported
+   */
+  report (err) {} // eslint-disable-line handle-callback-err
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_statelisteners_queryupdatelistener.js.html b/docs/core_statelisteners_queryupdatelistener.js.html new file mode 100644 index 000000000..6150c5ca7 --- /dev/null +++ b/docs/core_statelisteners_queryupdatelistener.js.html @@ -0,0 +1,145 @@ + + + + + JSDoc: Source: core/statelisteners/queryupdatelistener.js + + + + + + + + + + +
+ +

Source: core/statelisteners/queryupdatelistener.js

+ + + + + + +
+
+
import QueryTriggers from '../models/querytriggers';
+import UniversalResults from '../models/universalresults';
+import VerticalResults from '../models/verticalresults';
+import SearchOptionsFactory from '../search/searchoptionsfactory';
+import StorageKeys from '../storage/storagekeys';
+
+/**
+ * QueryUpdateListener is responsible for running a vertical or universal search when
+ * the QUERY storage key is updated.
+ */
+export default class QueryUpdateListener {
+  constructor (core, config) {
+    this.core = core;
+    this.config = {
+      searchCooldown: 300,
+      verticalKey: undefined,
+      allowEmptySearch: true,
+      defaultInitialSearch: undefined,
+      ...config
+    };
+
+    /**
+     * Middleware functions to be run before a search. Can be either async or sync.
+     */
+    this.middleware = [];
+
+    this.searchOptionsFactory = new SearchOptionsFactory();
+
+    this.core.storage.registerListener({
+      storageKey: StorageKeys.QUERY,
+      eventType: 'update',
+      callback: query => this._handleQueryUpdate(query)
+    });
+  }
+
+  /**
+   * Register a middleware, to be called before searches are run.
+   * Middleware must return a Promise if they are async.
+   *
+   * @param {Function} middlewareFunction
+   */
+  registerMiddleware (middlewareFunction) {
+    this.middleware.push(middlewareFunction);
+  }
+
+  /**
+   * Runs a debounced search. If the query is null, set the query to the defaultInitialSearch,
+   * which retriggers the QUERY listener.
+   *
+   * @param {string} query
+   */
+  _handleQueryUpdate (query) {
+    if (query === null) {
+      if (this.config.defaultInitialSearch || this.config.defaultInitialSearch === '') {
+        this.core.storage.set(StorageKeys.QUERY_TRIGGER, QueryTriggers.INITIALIZE);
+        this.core.storage.set(StorageKeys.QUERY, this.config.defaultInitialSearch);
+      }
+      return;
+    }
+    this._debouncedSearch(query);
+  }
+
+  _debouncedSearch (query) {
+    if (this._throttled ||
+      (!query && !this.config.allowEmptySearch &&
+        this.core.storage.get(StorageKeys.QUERY_TRIGGER) !== QueryTriggers.FILTER_COMPONENT)) {
+      return;
+    }
+    this._setSearchLoadingState();
+
+    this._throttled = true;
+    setTimeout(() => { this._throttled = false; }, this._searchCooldown);
+    return this._search(query);
+  }
+
+  _setSearchLoadingState () {
+    this.config.verticalKey
+      ? this.core.storage.set(StorageKeys.VERTICAL_RESULTS, VerticalResults.searchLoading())
+      : this.core.storage.set(StorageKeys.UNIVERSAL_RESULTS, UniversalResults.searchLoading());
+  }
+
+  _search (query) {
+    const middlewarePromises = this.middleware.map(middleware => middleware(query));
+    const queryTrigger = this.core.storage.get(StorageKeys.QUERY_TRIGGER);
+    return Promise.all(middlewarePromises).then(() => {
+      if (this.config.verticalKey) {
+        return this.core.verticalSearch(
+          this.config.verticalKey, this.searchOptionsFactory.create(queryTrigger), { input: query });
+      } else {
+        return this.core.search(query, this.searchOptionsFactory.create(queryTrigger));
+      }
+    });
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_storage_moduledata.js.html b/docs/core_storage_moduledata.js.html index 23eb87a83..bbcbd6166 100644 --- a/docs/core_storage_moduledata.js.html +++ b/docs/core_storage_moduledata.js.html @@ -43,7 +43,7 @@

Source: core/storage/moduledata.js

this._id = id; this._history = []; - this._data = {}; + this._data = data; this.set(data); } @@ -52,20 +52,22 @@

Source: core/storage/moduledata.js

* @param {*} data the data to replace the current data */ set (data) { - const newData = data || {}; - this.capturePrevious(); - if (Object.keys(newData).length !== Object.keys(this._data).length) { - this._data = newData; + if (data === null || + typeof data !== 'object' || + Array.isArray(data) || + Object.keys(data).length !== Object.keys(this._data).length + ) { + this._data = data; this.emit('update', this._data); return; } // check for shallow equality - for (const key of Object.keys(newData)) { - if (this._data[key] !== newData[key]) { - this._data = newData; + for (const key of Object.keys(data)) { + if (this._data[key] !== data[key]) { + this._data = data; this.emit('update', this._data); return; } @@ -108,13 +110,13 @@

Source: core/storage/moduledata.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/core_storage_resultscontext.js.html b/docs/core_storage_resultscontext.js.html new file mode 100644 index 000000000..17771eca6 --- /dev/null +++ b/docs/core_storage_resultscontext.js.html @@ -0,0 +1,63 @@ + + + + + JSDoc: Source: core/storage/resultscontext.js + + + + + + + + + + +
+ +

Source: core/storage/resultscontext.js

+ + + + + + +
+
+
/** @module ResultsContext */
+
+/**
+ * ResultsContext is an ENUM that provides context
+ * for the results that we are storing from server
+ * data
+ * @enum {string}
+ */
+export default {
+  NORMAL: 'normal',
+  NO_RESULTS: 'no-results'
+};
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_storage_searchstates.js.html b/docs/core_storage_searchstates.js.html new file mode 100644 index 000000000..fdea65eab --- /dev/null +++ b/docs/core_storage_searchstates.js.html @@ -0,0 +1,68 @@ + + + + + JSDoc: Source: core/storage/searchstates.js + + + + + + + + + + +
+ +

Source: core/storage/searchstates.js

+ + + + + + +
+
+
/** @module SearchStates */
+
+/**
+ * @typedef {string} SearchState
+ */
+
+/**
+ * SearchStates is an ENUM for the various stages of searching,
+ * used to show different templates
+ * @enum {string}
+ */
+const SearchStates = {
+  PRE_SEARCH: 'pre-search',
+  SEARCH_LOADING: 'search-loading',
+  SEARCH_COMPLETE: 'search-complete'
+};
+export default SearchStates;
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_storage_storage.js.html b/docs/core_storage_storage.js.html index 776cdff1c..4af95cc4c 100644 --- a/docs/core_storage_storage.js.html +++ b/docs/core_storage_storage.js.html @@ -26,108 +26,253 @@

Source: core/storage/storage.js

-
/** @module Storage */
-
-import ModuleData from './moduledata';
+            
import DefaultPersistentStorage from '@yext/answers-storage';
 import { AnswersStorageError } from '../errors/errors';
+import SearchParams from '../../ui/dom/searchparams';
+
+/** @typedef {import('./storagelistener').default} StorageListener */
 
 /**
- * Storage is a container around application state.
- * It exposes an interface for CRUD operations as well as listening
+ * Storage is a container around application state.  It
+ * exposes an interface for CRUD operations as well as listening
  * for stateful changes.
+ *
+ * @param {Function} callback for state (persistent store) updates
+ * @param {Function} callback for state (persistent store) reset
  */
 export default class Storage {
-  constructor () {
-    this._moduleDataContainer = {};
-    this._futureListeners = {};
+  constructor (config = {}) {
+    /**
+     * The listeners for changes in state (persistent storage changes)
+     */
+    this.persistedStateListeners = {
+      update: config.updateListener || function () {},
+      reset: config.resetListener || function () {}
+    };
+
+    /**
+     * A hook for parsing values from persistent storage on init.
+     *
+     * @type {Function}
+     */
+    this.persistedValueParser = config.persistedValueParser;
+
+    /**
+     * The listener for window.pop in the persistent storage
+     *
+     * @param {Map<string, string>} queryParamsMap A Map containing the persisted state,
+     *                                             for example a map of 'query' => 'virginia'
+     * @param {string} queryParamsString the url params of the persisted state
+     *                                   for the above case 'query=virginia'
+     */
+    this.popListener = (queryParamsMap, queryParamsString) => {
+      this.persistedStateListeners.update(queryParamsMap, queryParamsString);
+      this.persistedStateListeners.reset(queryParamsMap, queryParamsString);
+    };
+
+    /**
+     * The core data for the storage
+     *
+     * @type {Map<string, *>}
+     */
+    this.storage = new Map();
+
+    /**
+     * The persistent storage implementation to store state
+     * across browser sessions and URLs
+     *
+     * @type {DefaultPersistentStorage}
+     */
+    this.persistentStorage = new DefaultPersistentStorage(this.popListener);
+
+    /**
+     * The listeners to apply on changes to storage
+     *
+     * @type {StorageListener[]}
+     */
+    this.listeners = [];
   }
 
   /**
-   * Set the data in storage with the given key to the provided data,
-   * completely overwriting any existing data.
-   * @param {string} key the storage key to set
-   * @param {*} data the data to set
+   * Decodes the initial state from the query params. This could be a
+   * direct mapping from query param to storage keys in the storage or
+   * could fetch a sessionId from some backend
+   *
+   * @param {string} url The starting URL
+   * @returns {Storage}
    */
-  set (key, data) {
-    this._initDataContainer(key, data);
-    this._moduleDataContainer[key].set(data);
+  init (url) {
+    this.persistentStorage.init(url);
+    this.persistentStorage.getAll().forEach((value, key) => {
+      const parsedValue = this.persistedValueParser
+        ? this.persistedValueParser(key, value)
+        : value;
+      this.set(key, parsedValue);
+    });
+    return this;
   }
 
-  _initDataContainer (key, data) {
+  /**
+   * Set the data in storage with the given key to the provided
+   * data, completely overwriting any existing data.
+   *
+   * @param {string} key The storage key to set
+   * @param {*} data The data to set
+   */
+  set (key, data) {
     if (key === undefined || key === null || typeof key !== 'string') {
-      throw new AnswersStorageError('Invalid storage key provided', key, data);
-    }
-    if (data === undefined || data === null) {
-      throw new AnswersStorageError('No data provided', key, data);
+      throw new AnswersStorageError('Storage key must be of type string', key, data);
     }
 
-    if (this._moduleDataContainer[key] === undefined) {
-      this._moduleDataContainer[key] = new ModuleData(key);
-      this._applyFutureListeners(key);
+    if (typeof data === 'undefined') {
+      throw new AnswersStorageError('Data cannot be of type undefined', key, data);
     }
+
+    this.storage.set(key, data);
+    this._callListeners('update', key);
   }
 
-  getState (moduleId) {
-    if (this._moduleDataContainer[moduleId]) {
-      return this._moduleDataContainer[moduleId].raw();
+  /**
+   * Updates the storage with a new entry of [key, data].  The entry
+   * is not added to the URL until the history is updated.
+   *
+   * @param {string} key The storage key to set
+   * @param {*} data The data to set
+   */
+  setWithPersist (key, data) {
+    this.set(key, data);
+
+    let serializedData = data;
+    if (typeof data !== 'string') {
+      serializedData = JSON.stringify(data);
     }
-    return {};
+
+    this.persistentStorage.set(key, serializedData);
   }
 
-  getAll (key) {
-    const data = [];
-    for (const dataKey of Object.keys(this._moduleDataContainer)) {
-      if (dataKey.startsWith(key) && this._moduleDataContainer[dataKey].raw() !== null) {
-        data.push(this._moduleDataContainer[dataKey].raw());
-      }
-    }
-    return data;
+  /**
+   * Adds all entries of the persistent storage to the URL,
+   * replacing the current history state.
+   */
+  replaceHistoryWithState () {
+    this.persistentStorage.replaceHistoryWithState();
+    this.persistedStateListeners.update(
+      this.persistentStorage.getAll(),
+      this.getCurrentStateUrlMerged()
+    );
   }
 
-  on (evt, moduleId, cb) {
-    let moduleData = this._moduleDataContainer[moduleId];
-    if (moduleData === undefined) {
-      if (this._futureListeners[moduleId] === undefined) {
-        this._futureListeners[moduleId] = [];
-      }
+  /**
+   * Adds all entries of the persistent storage to the URL.
+   */
+  pushStateToHistory () {
+    this.persistentStorage.pushStateToHistory();
+    this.persistedStateListeners.update(
+      this.persistentStorage.getAll(),
+      this.getCurrentStateUrlMerged()
+    );
+  }
 
-      this._futureListeners[moduleId].push({
-        event: evt,
-        cb: cb
-      });
+  /**
+   * Get the current state for the provided key
+   *
+   * @param {string} key The storage key to get
+   * @return {*} The state for the provided key, undefined if key doesn't exist
+   */
+  get (key) {
+    return this.storage.get(key);
+  }
+
+  /**
+   * Get the current state for all key/value pairs in storage
+   *
+   * @return {Map<string, *>} mapping from key to value representing the current state
+   */
+  getAll () {
+    return new Map(this.storage);
+  }
 
-      return;
+  /**
+   * Remove the data in storage with the given key
+   *
+   * @param {string} key The storage key to delete
+   */
+  delete (key) {
+    if (key === undefined || key === null || typeof key !== 'string') {
+      throw new AnswersStorageError('Storage key must be of type string', key);
     }
 
-    this._moduleDataContainer[moduleId].on(evt, cb);
-    return this;
+    this.storage.delete(key);
+    this.persistentStorage.delete(key);
   }
 
-  off (evt, moduleId, cb) {
-    let moduleData = this._moduleDataContainer[moduleId];
-    if (moduleData === undefined) {
-      if (this._futureListeners[moduleId] !== undefined) {
-        this._futureListeners[moduleId].pop();
-      }
+  /**
+   * Whether the specified key exists or not
+   *
+   * @param {string} key the storage key
+   * @return {boolean}
+   */
+  has (key) {
+    return this.storage.has(key);
+  }
 
-      return this;
-    }
+  /**
+   * Returns the url representing the current persisted state, merged
+   * with any additional query params currently in the url.
+   *
+   * @returns {string}
+   */
+  getCurrentStateUrlMerged () {
+    const searchParams = new SearchParams(window.location.search.substring(1));
+    this.persistentStorage.getAll().forEach((value, key) => {
+      searchParams.set(key, value);
+    });
+    return searchParams.toString();
+  }
 
-    this._moduleDataContainer[moduleId].off(evt, cb);
-    return this;
+  /**
+   * Returns the query parameters to encode the current state
+   *
+   * @return {string} The query parameters for a page link with the
+   *                  current state encoded
+   *                  e.g. query=all&context=%7Bkey:'hello'%7D
+   */
+  getUrlWithCurrentState () {
+    return this.persistentStorage.getUrlWithCurrentState();
   }
 
-  _applyFutureListeners (moduleId) {
-    let futures = this._futureListeners[moduleId];
-    if (!futures) {
-      return;
+  /**
+   * Adds a listener to the given module for a given event
+   *
+   * @param {StorageListener} listener the listener to add
+   */
+  registerListener (listener) {
+    if (!listener.eventType || !listener.storageKey ||
+      !listener.callback || typeof listener.callback !== 'function') {
+      throw new AnswersStorageError(`Invalid listener applied in storage: ${listener}`);
     }
+    this.listeners.push(listener);
+  }
 
-    for (let i = 0; i < futures.length; i++) {
-      let future = futures[i];
-      this.on(future.event, moduleId, future.cb);
-    }
-    delete this._futureListeners[moduleId];
+  /**
+   * Removes a given listener from the set of listeners
+   *
+   * @param {StorageListener} listener the listener to remove
+   */
+  removeListener (listener) {
+    this.listeners = this.listeners.filter(l => l !== listener);
+  }
+
+  /**
+   * @param {string} eventType
+   * @param {string} storageKey
+   */
+  _callListeners (eventType, storageKey) {
+    this.listeners.forEach((listener) => {
+      if (listener.storageKey === storageKey && listener.eventType === eventType) {
+        listener.callback(this.get(storageKey));
+      }
+    });
   }
 }
 
@@ -140,13 +285,13 @@

Source: core/storage/storage.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/core_storage_storageindexes.js.html b/docs/core_storage_storageindexes.js.html new file mode 100644 index 000000000..c3e1df7f9 --- /dev/null +++ b/docs/core_storage_storageindexes.js.html @@ -0,0 +1,103 @@ + + + + + JSDoc: Source: core/storage/storageindexes.js + + + + + + + + + + +
+ +

Source: core/storage/storageindexes.js

+ + + + + + +
+
+
/** @module */
+
+/**
+ * StorageIndexes is an ENUM are considered the root context
+ * for how data is stored and scoped in the storage.
+ *
+ * @enum {string}
+ */
+export default {
+  /**
+   * The global index that should contain all application
+   * specific globals for the application (e.g. search params)
+   */
+  GLOBAL: 'global',
+
+  /**
+   * The Navigation index contains all data to power the navigation component.
+   * Sometimes other components might depend directly on this as well, but
+   * we've opted to try to store some of that data in the global index instead.
+   */
+  NAVIGATION: 'navigation',
+
+  /**
+   * The Universal Results index for all data related to search results
+   * for universal search
+   */
+  UNIVERSAL_RESULTS: 'universal-results',
+
+  /**
+   * The Vertical Results index for all data related to search results
+   * for vertical search
+   */
+  VERTICAL_RESULTS: 'vertical-results',
+
+  /**
+   * The Autocomplete index contains state to power the auto complete component
+   * This data is powered by network requests for both vertical and universal.
+   */
+  AUTOCOMPLETE: 'autocomplete',
+
+  /**
+   * The direct answer index contains all the data to power the Direct Answer component
+   * Typically this index is powered from universal results, in the response to a search query
+   */
+  DIRECT_ANSWER: 'direct-answer',
+
+  /**
+   * The Filter index is the global source of truth for all filters on a page.
+   * It should contain all the latest state that is used for search.
+   */
+  FILTER: 'filter'
+};
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_storage_storagekeys.js.html b/docs/core_storage_storagekeys.js.html index 82df93318..2bff2b8ac 100644 --- a/docs/core_storage_storagekeys.js.html +++ b/docs/core_storage_storagekeys.js.html @@ -26,7 +26,7 @@

Source: core/storage/storagekeys.js

-
/** @module */
+            
/** @module StorageKeys */
 
 /**
  * StorageKeys is an ENUM are considered the root context
@@ -34,15 +34,44 @@ 

Source: core/storage/storagekeys.js

* * @enum {string} */ -export default { +const StorageKeys = { NAVIGATION: 'navigation', UNIVERSAL_RESULTS: 'universal-results', VERTICAL_RESULTS: 'vertical-results', + ALTERNATIVE_VERTICALS: 'alternative-verticals', AUTOCOMPLETE: 'autocomplete', DIRECT_ANSWER: 'direct-answer', - FILTER: 'filter', - QUERY: 'query' + FILTER: 'filter', // DEPRECATED + PERSISTED_FILTER: 'filters', + STATIC_FILTER_NODES: 'static-filter-nodes', + LOCATION_RADIUS_FILTER_NODE: 'location-radius-filter-node', + PERSISTED_LOCATION_RADIUS: 'locationRadius', + QUERY: 'query', + QUERY_ID: 'query-id', + FACET_FILTER_NODES: 'facet-filter-nodes', + PERSISTED_FACETS: 'facetFilters', + DYNAMIC_FILTERS: 'dynamic-filters', + GEOLOCATION: 'geolocation', + QUESTION_SUBMISSION: 'question-submission', + SEARCH_CONFIG: 'search-config', + SEARCH_OFFSET: 'search-offset', + SPELL_CHECK: 'spell-check', + SKIP_SPELL_CHECK: 'skipSpellCheck', + LOCATION_BIAS: 'location-bias', + SESSIONS_OPT_IN: 'sessions-opt-in', + VERTICAL_PAGES_CONFIG: 'vertical-pages-config', + LOCALE: 'locale', + SORT_BYS: 'sortBys', + NO_RESULTS_CONFIG: 'no-results-config', + RESULTS_HEADER: 'results-header', // DEPRECATED + API_CONTEXT: 'context', + REFERRER_PAGE_URL: 'referrerPageUrl', + QUERY_TRIGGER: 'queryTrigger', + FACETS_LOADED: 'facets-loaded', + QUERY_SOURCE: 'query-source', + HISTORY_POP_STATE: 'history-pop-state' }; +export default StorageKeys;
@@ -53,13 +82,13 @@

Source: core/storage/storagekeys.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/core_storage_storagelistener.js.html b/docs/core_storage_storagelistener.js.html new file mode 100644 index 000000000..6d4c162c8 --- /dev/null +++ b/docs/core_storage_storagelistener.js.html @@ -0,0 +1,66 @@ + + + + + JSDoc: Source: core/storage/storagelistener.js + + + + + + + + + + +
+ +

Source: core/storage/storagelistener.js

+ + + + + + +
+
+
/**
+ * The storage listener is a listener on changes to the SDK storage
+ */
+export default class StorageListener {
+  /**
+   * @param {string} eventType The type of event to listen for e.g. 'update'
+   * @param {string} storageKey The key to listen for e.g. 'Pagination'
+   * @param {Function} callback The callback to call when the event is emitted on the storage key
+   */
+  constructor (eventType, storageKey, callback) {
+    this.eventType = eventType;
+    this.storageKey = storageKey;
+    this.callback = callback;
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_utils_arrayutils.js.html b/docs/core_utils_arrayutils.js.html new file mode 100644 index 000000000..8740686ea --- /dev/null +++ b/docs/core_utils_arrayutils.js.html @@ -0,0 +1,76 @@ + + + + + JSDoc: Source: core/utils/arrayutils.js + + + + + + + + + + +
+ +

Source: core/utils/arrayutils.js

+ + + + + + +
+
+

+/**
+ * Groups an array into an object using a given key and value function, and an initial object
+ * to add to. By default the key and value functions will not perform any transformations
+ * on the array elements.
+ * @param {Array<any>} arr array to be grouped
+ * @param {Function} keyFunc function that evaluates what key to give an array element.
+ * @param {Function} valueFunc function that evaluates what value to give an array element.
+ * @param {Object} intitial the initial object to add to, defaulting to {}
+ * @returns {Object}
+ */
+export function groupArray (arr, keyFunc, valueFunc, initial) {
+  keyFunc = keyFunc || (key => key);
+  valueFunc = valueFunc || (value => value);
+  return arr.reduce((groups, element, idx) => {
+    const key = keyFunc(element, idx);
+    const value = valueFunc(element, idx);
+    if (!groups[key]) {
+      groups[key] = [ value ];
+    } else {
+      groups[key].push(value);
+    }
+    return groups;
+  }, initial || {});
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_utils_configutils.js.html b/docs/core_utils_configutils.js.html new file mode 100644 index 000000000..795f73365 --- /dev/null +++ b/docs/core_utils_configutils.js.html @@ -0,0 +1,83 @@ + + + + + JSDoc: Source: core/utils/configutils.js + + + + + + + + + + +
+ +

Source: core/utils/configutils.js

+ + + + + + +
+
+
/**
+ * Used to parse config options, defaulting to different synonyms and
+ * finally a default value. Option names with periods will be parsed
+ * as multiple child object accessors, i.e. trying to access 'first.second.option'
+ * will first look for config['first']['second']['option'].
+ *
+ * This is mostly needed for boolean config values, since boolean operators,
+ * which we commonly use for defaulting config options, do not work properly
+ * in those cases.
+ * @param {Object} config
+ * @param {Array<string>}
+ * @param {any} defaultValue
+ */
+export function defaultConfigOption (config, synonyms, defaultValue) {
+  for (let name of synonyms) {
+    const accessors = name.split('.');
+    let parentConfig = config;
+    let skip = false;
+    for (let childConfigAccessor of accessors.slice(0, -1)) {
+      if (!(childConfigAccessor in parentConfig)) {
+        skip = true;
+        break;
+      }
+      parentConfig = parentConfig[childConfigAccessor];
+    }
+    const configName = accessors[accessors.length - 1];
+    if (!skip && configName in parentConfig) {
+      return parentConfig[configName];
+    }
+  }
+  return defaultValue;
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_utils_filternodeutils.js.html b/docs/core_utils_filternodeutils.js.html new file mode 100644 index 000000000..c8b4e862f --- /dev/null +++ b/docs/core_utils_filternodeutils.js.html @@ -0,0 +1,98 @@ + + + + + JSDoc: Source: core/utils/filternodeutils.js + + + + + + + + + + +
+ +

Source: core/utils/filternodeutils.js

+ + + + + + +
+
+
import FilterNodeFactory from '../filters/filternodefactory';
+import Filter from '../models/filter';
+import FilterMetadata from '../filters/filtermetadata';
+
+/**
+ * Converts an array of {@link AppliedQueryFilter}s into equivalent {@link SimpleFilterNode}s.
+ * @param {Array<AppliedQueryFilter>} nlpFilters
+ * @returns {Array<SimpleFilterNode>}
+ */
+export function convertNlpFiltersToFilterNodes (nlpFilters) {
+  return nlpFilters.map(nlpFilter => FilterNodeFactory.from({
+    filter: Filter.from(nlpFilter.filter),
+    metadata: new FilterMetadata({
+      fieldName: nlpFilter.key,
+      displayValue: nlpFilter.value
+    })
+  }));
+}
+
+/**
+ * Flattens an array of {@link FilterNode}s into an array
+ * of their constituent leaf {@link SimpleFilterNode}s.
+ * @param {Array<FilterNode>} filterNodes
+ * @returns {Array<SimpleFilterNode>}
+ */
+export function flattenFilterNodes (filterNodes) {
+  return filterNodes.flatMap(fn => fn.getSimpleDescendants());
+}
+
+/**
+ * Returns the given array of {@link FilterNode}s,
+ * removing FilterNodes that are empty or have a field id listed as a hidden.
+ * @param {Array<FilterNode>} filterNodes
+ * @param {Array<string>} hiddenFields
+ * @returns {Array<FilterNode>}
+ */
+export function pruneFilterNodes (filterNodes, hiddenFields) {
+  return filterNodes
+    .filter(fn => {
+      const { fieldName, displayValue } = fn.getMetadata();
+      if (!fieldName || !displayValue) {
+        return false;
+      }
+      const fieldId = fn.getFilter().getFilterKey();
+      return !hiddenFields.includes(fieldId);
+    });
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_utils_objectutils.js.html b/docs/core_utils_objectutils.js.html new file mode 100644 index 000000000..0a9339e34 --- /dev/null +++ b/docs/core_utils_objectutils.js.html @@ -0,0 +1,72 @@ + + + + + JSDoc: Source: core/utils/objectutils.js + + + + + + + + + + +
+ +

Source: core/utils/objectutils.js

+ + + + + + +
+
+
/**
+ * Nest a value inside an object whose structure is defined by an array of keys
+ *
+ * Example: if `value` is 'Hello, world!', and `keys` is ['a', 'b'],
+ * the function will return the object:
+ *
+ * {
+ *   a: {
+ *     b: 'Hello, world!'
+ *   }
+ * }
+ *
+ * @param {*} value
+ * @param {string[]} keys
+ * @returns {Object}
+ */
+export function nestValue (value, keys) {
+  return keys.reduceRight((acc, key) => {
+    return { [key]: acc };
+  }, value);
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_utils_resultsutils.js.html b/docs/core_utils_resultsutils.js.html new file mode 100644 index 000000000..bd2e5b38d --- /dev/null +++ b/docs/core_utils_resultsutils.js.html @@ -0,0 +1,71 @@ + + + + + JSDoc: Source: core/utils/resultsutils.js + + + + + + + + + + +
+ +

Source: core/utils/resultsutils.js

+ + + + + + +
+
+
import SearchStates from '../storage/searchstates';
+
+/**
+ * Returns a CSS class for the input searchState
+ * @param {SearchState} searchState
+ * @returns {string}
+ */
+export function getContainerClass (searchState) {
+  switch (searchState) {
+    case SearchStates.PRE_SEARCH:
+      return 'yxt-Results--preSearch';
+    case SearchStates.SEARCH_LOADING:
+      return 'yxt-Results--searchLoading';
+    case SearchStates.SEARCH_COMPLETE:
+      return 'yxt-Results--searchComplete';
+    default:
+      console.trace(`encountered an unknown search state: ${searchState}`);
+      return '';
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_utils_richtextformatter.js.html b/docs/core_utils_richtextformatter.js.html new file mode 100644 index 000000000..55ef015d8 --- /dev/null +++ b/docs/core_utils_richtextformatter.js.html @@ -0,0 +1,144 @@ + + + + + JSDoc: Source: core/utils/richtextformatter.js + + + + + + + + + + +
+ +

Source: core/utils/richtextformatter.js

+ + + + + + +
+
+
import iterator from 'markdown-it-for-inline';
+import RtfConverter from '@yext/rtf-converter';
+import { AnswersCoreError } from '../errors/errors';
+
+/**
+ * This class leverages the {@link RtfConverter} library to perform Rich Text to
+ * HTML conversions.
+ */
+class RichTextFormatterImpl {
+  /**
+   * Generates an HTML representation of the provided Rich Text field value. Note that
+   * the HTML will contain a wrapper div. This is to support click analytics for Rich Text
+   * links.
+   *
+   * @param {string} fieldValue A Rich Text field value.
+   * @param {string} fieldName The name of the field, to be included in the payload of a click
+   *                           analytics event. This parameter is optional.
+   * @param {Object|string} targetConfig Configuration object specifying the 'target' behavior for
+   *                          the various types of links. If a string is provided, it is assumed that
+   *                          is the 'target' behavior across all types of links. This parameter is optional.
+   * @returns {string} The HTML representation of the field value, serialized as a string.
+   */
+  format (fieldValue, fieldName, targetConfig) {
+    if (typeof fieldValue !== 'string') {
+      throw new AnswersCoreError(
+        `Rich text "${fieldValue}" needs to be a string. Currently is a ${typeof fieldValue}`
+      );
+    }
+
+    const pluginName = this._generatePluginName();
+    RtfConverter.addPlugin(
+      iterator,
+      pluginName,
+      'link_open',
+      (tokens, idx) => this._urlTransformer(tokens, idx, targetConfig));
+
+    fieldName = fieldName || '';
+    const html =
+      `<div class="js-yxt-rtfValue" data-field-name="${fieldName}">\n` +
+      `${RtfConverter.toHTML(fieldValue)}` +
+      '</div>';
+
+    // Because all invocations of this method share the same {@link RtfConverter}, we must make sure to
+    // disable the plugin added above. Otherwise, it will be applied in all subsequent conversions.
+    RtfConverter.disablePlugin(pluginName);
+
+    return html;
+  }
+
+  /**
+   * An inline token parser for use with the {@link iterator} Markdown-it plugin.
+   * This token parser adds a cta-type data attribute to any link it encounters.
+   */
+  _urlTransformer (tokens, idx, targetConfig) {
+    targetConfig = targetConfig || {};
+    let target;
+    if (typeof targetConfig === 'string') {
+      target = targetConfig;
+    }
+
+    const href = tokens[idx].attrGet('href');
+    let ctaType;
+    if (href.startsWith('mailto')) {
+      ctaType = 'EMAIL';
+      target = target || targetConfig.email;
+    } else if (href.startsWith('tel')) {
+      ctaType = 'TAP_TO_CALL';
+      target = target || targetConfig.phone;
+    } else {
+      ctaType = 'VIEW_WEBSITE';
+      target = target || targetConfig.url;
+    }
+
+    tokens[idx].attrSet('data-cta-type', ctaType);
+    target && tokens[idx].attrSet('target', target);
+  }
+
+  /**
+   * A function that generates a unique UUID to serve as the name for a
+   * Markdown-it plugin.
+   *
+   * @returns {string} the UUID.
+   */
+  _generatePluginName () {
+    function s4 () {
+      return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
+    }
+    return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
+  }
+}
+
+const RichTextFormatter = new RichTextFormatterImpl();
+export default RichTextFormatter;
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_utils_strings.js.html b/docs/core_utils_strings.js.html new file mode 100644 index 000000000..368a54c8a --- /dev/null +++ b/docs/core_utils_strings.js.html @@ -0,0 +1,82 @@ + + + + + JSDoc: Source: core/utils/strings.js + + + + + + + + + + +
+ +

Source: core/utils/strings.js

+ + + + + + +
+
+
/**
+   * Truncates strings to 250 characters, attempting to preserve whole words
+   * @param str {string} the string to truncate
+   * @param limit {Number} the maximum character length to return
+   * @param trailing {string} a trailing string to denote truncation, e.g. '...'
+   * @param sep {string} the word separator
+   * @returns {string}
+   */
+export function truncate (str, limit = 250, trailing = '...', sep = ' ') {
+  if (!str || str.length <= limit) {
+    return str;
+  }
+
+  // TODO (bmcginnis): split punctuation too so we don't end up with "foo,..."
+  const words = str.split(sep);
+  const max = limit - trailing.length;
+  let truncated = '';
+
+  for (let i = 0; i < words.length; i++) {
+    const word = words[i];
+    if (truncated.length + word.length > max ||
+      (i !== 0 && truncated.length + word.length + sep.length > max)) {
+      truncated += trailing;
+      break;
+    }
+
+    truncated += i === 0 ? word : sep + word;
+  }
+
+  return truncated;
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/core_utils_urlutils.js.html b/docs/core_utils_urlutils.js.html new file mode 100644 index 000000000..2340a2402 --- /dev/null +++ b/docs/core_utils_urlutils.js.html @@ -0,0 +1,195 @@ + + + + + JSDoc: Source: core/utils/urlutils.js + + + + + + + + + + +
+ +

Source: core/utils/urlutils.js

+ + + + + + +
+
+
import { PRODUCTION, SANDBOX } from '../constants';
+import SearchParams from '../../ui/dom/searchparams';
+import StorageKeys from '../storage/storagekeys';
+import ComponentTypes from '../../ui/components/componenttypes';
+
+/**
+ * Returns the base url for the live api backend in the desired environment.
+ * @param {string} env The desired environment.
+ */
+export function getLiveApiUrl (env = PRODUCTION) {
+  return env === SANDBOX ? 'https://liveapi-sandbox.yext.com' : 'https://liveapi.yext.com';
+}
+
+/**
+ * Returns the base url for the live api backend in the desired environment.
+ * @param {string} env The desired environment.
+ */
+export function getCachedLiveApiUrl (env = PRODUCTION) {
+  return env === SANDBOX ? 'https://liveapi-sandbox.yext.com' : 'https://liveapi-cached.yext.com';
+}
+
+/**
+ * Returns the base url for the knowledge api backend in the desired environment.
+ * @param {string} env The desired environment.
+ */
+export function getKnowledgeApiUrl (env = PRODUCTION) {
+  return env === SANDBOX ? 'https://api-sandbox.yext.com' : 'https://api.yext.com';
+}
+
+/**
+ * Returns the base url for the analytics backend in the desired environment.
+ * @param {string} env The desired environment.
+ * @param {boolean} conversionTrackingEnabled If conversion tracking has been opted into.
+ */
+export function getAnalyticsUrl (env = PRODUCTION, conversionTrackingEnabled = false) {
+  if (conversionTrackingEnabled) {
+    return env === SANDBOX
+      ? 'https://sandbox-realtimeanalytics.yext.com'
+      : 'https://realtimeanalytics.yext.com';
+  }
+  return env === SANDBOX
+    ? 'https://sandbox-answers.yext-pixel.com'
+    : 'https://answers.yext-pixel.com';
+}
+
+/**
+ * Returns the passed in url with the passed in params appended as query params
+ * Note: query parameters in the url are stripped, you should include those query parameters
+ * in `params` if you want to keep them
+ * @param {string} url
+ * @param {SearchParams} params to add to the url
+ * @returns {string}
+ */
+export function replaceUrlParams (url, params = new SearchParams()) {
+  return url.split('?')[0] + '?' + params.toString();
+}
+
+/**
+ * Returns the given url without query params and hashes
+ * @param {string} url Full url e.g. https://yext.com/?query=hello#Footer
+ * @returns {string} Url without query params and hashes e.g. https://yext.com/
+ */
+export function urlWithoutQueryParamsAndHash (url) {
+  return url.split('?')[0].split('#')[0];
+}
+
+/**
+ * returns if two SearchParams objects have the same key,value entries
+ * @param {SearchParams} params1
+ * @param {SearchParams} params2
+ * @return {boolean} true if params1 and params2 have the same key,value entries, false otherwise
+ */
+export function equivalentParams (params1, params2) {
+  const entries1 = Array.from(params1.entries());
+  const entries2 = Array.from(params2.entries());
+
+  if (entries1.length !== entries2.length) {
+    return false;
+  }
+  for (const [key, val] of params1.entries()) {
+    if (val !== params2.get(key)) {
+      return false;
+    }
+  }
+  return true;
+}
+
+/**
+ * Creates a copy of the provided {@link SearchParams}, with the specified
+ * attributes filtered out
+ * @param {SearchParams} params The parameters to remove from
+ * @param {string[]} prefixes The prefixes of parameters to remove
+ * @return {SearchParams} A new instance of SearchParams without entries with
+ *   keys that start with the given prefixes
+ */
+export function removeParamsWithPrefixes (params, prefixes) {
+  const newParams = new SearchParams();
+  for (const [key, val] of params.entries()) {
+    const includeEntry = prefixes.every(prefix => !key.startsWith(prefix));
+    if (includeEntry) {
+      newParams.set(key, val);
+    }
+  }
+  return newParams;
+}
+
+/**
+ * Removes parameters for filters, facets, sort options, and pagination
+ * from the provided {@link SearchParams}. This is useful for constructing
+ * inter-experience answers links.
+ * @param {SearchParams} params The parameters to remove from
+ * @param {function} getComponentNamesForComponentTypes Given string[]
+ *   component types, returns string[] component names for those types
+ * @return {SearchParams} Parameters that have filtered out params that
+ *   should not persist across the answers experience
+ */
+export function filterParamsForExperienceLink (
+  params,
+  getComponentNamesForComponentTypes
+) {
+  const componentTypesToExclude = [
+    ComponentTypes.GEOLOCATION_FILTER,
+    ComponentTypes.FILTER_SEARCH
+  ];
+  const paramsToFilter = componentTypesToExclude.reduce((params, type) => {
+    getComponentNamesForComponentTypes([type])
+      .forEach(componentName => {
+        params.push(`${StorageKeys.QUERY}.${componentName}`);
+        params.push(`${StorageKeys.FILTER}.${componentName}`);
+      });
+    return params;
+  }, []);
+
+  const newParams = removeParamsWithPrefixes(params, paramsToFilter);
+  const paramsToDelete = [
+    StorageKeys.SEARCH_OFFSET,
+    StorageKeys.PERSISTED_FILTER,
+    StorageKeys.PERSISTED_LOCATION_RADIUS,
+    StorageKeys.PERSISTED_FACETS,
+    StorageKeys.SORT_BYS
+  ];
+  paramsToDelete.forEach(storageKey => newParams.delete(storageKey));
+  return newParams;
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/global.html b/docs/global.html index f8de045c1..38c51001e 100644 --- a/docs/global.html +++ b/docs/global.html @@ -94,17 +94,8607 @@

+

Members

+ + +

(constant) Matcher

+ + + + +
+ A Matcher is a filtering operation for Filters. +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

_callListeners(eventType, storageKey)

+ + + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
eventType + + +string + + + +
storageKey + + +string + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

_generateArrayOneToN(language) → {Array}

+ + + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
language + + +string + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ an array of the form [0, 1, 2, ..., nPluralForms] +
+ + + +
+
+ Type +
+
+ +Array + + +
+
+ + + + + + + + + + + + + +

_handleQueryUpdate(query)

+ + + + + + +
+ Runs a debounced search. If the query is null, set the query to the defaultInitialSearch, +which retriggers the QUERY listener. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
query + + +string + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

_selectPluralForm(translations, count, language) → {string}

+ + + + + + +
+ Returns the correct plural form given a translations object and count. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
translations + + +Object + + + +
count + + +number + + + +
language + + +string + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +string + + +
+
+ + + + + + + + + + + + + +

addOptions(options)

+ + + + + + +
+ Adds the provided options to the event +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
options + + +object + + + + Additional options for the event
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

computeHighlightedDataRecursively(highlightedFields) → {AppliedHighlightedFields}

+ + + + + + +
+ Given an answers-core HighlightedFields tree, returns a new tree +with highlighting applied to the leaf nodes. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
highlightedFields + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +AppliedHighlightedFields + + +
+
+ + + + + + + + + + + + + +

convertNlpFiltersToFilterNodes(nlpFilters) → {Array.<SimpleFilterNode>}

+ + + + + + +
+ Converts an array of AppliedQueryFilters into equivalent SimpleFilterNodes. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
nlpFilters + + +Array.<AppliedQueryFilter> + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Array.<SimpleFilterNode> + + +
+
+ + + + + + + + + + + + + +

create() → {Object}

+ + + + + + +
+ Given a QUERY_TRIGGER, return the search options for the given trigger. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Object + + +
+
+ + + + + + + + + + + + + +

defaultConfigOption(config, synonyms, defaultValue)

+ + + + + + +
+ Used to parse config options, defaulting to different synonyms and +finally a default value. Option names with periods will be parsed +as multiple child object accessors, i.e. trying to access 'first.second.option' +will first look for config['first']['second']['option']. + +This is mostly needed for boolean config values, since boolean operators, +which we commonly use for defaulting config options, do not work properly +in those cases. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
config + + +Object + + + +
synonyms + + +Array.<string> + + + +
defaultValue + + +any + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

defaultTemplateName() → {string}

+ + + + + + +
+ The template to render +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +string + + +
+
+ + + + + + + + + + + + + +

delete(key)

+ + + + + + +
+ Remove the data in storage with the given key +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
key + + +string + + + + The storage key to delete
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

equivalentParams(params1, params2) → {boolean}

+ + + + + + +
+ returns if two SearchParams objects have the same key,value entries +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
params1 + + +SearchParams + + + +
params2 + + +SearchParams + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ true if params1 and params2 have the same key,value entries, false otherwise +
+ + + +
+
+ Type +
+
+ +boolean + + +
+
+ + + + + + + + + + + + + +

filterIsPersisted(filter, persistedFilter) → {boolean}

+ + + + + + +
+ Checks whether a filter is equal to or included somewhere within the persistedFilter. +Assumes the given filter is a simple filter, i.e. does not have any child filters. +The persistedFilter can be either combined or simple. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
filter + + +Filter + + + +
persistedFilter + + +Filter + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +boolean + + +
+
+ + + + + + + + + + + + + + + + + + + + +
+ Removes parameters for filters, facets, sort options, and pagination +from the provided SearchParams. This is useful for constructing +inter-experience answers links. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
params + + +SearchParams + + + + The parameters to remove from
getComponentNamesForComponentTypes + + +function + + + + Given string[] + component types, returns string[] component names for those types
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Parameters that have filtered out params that + should not persist across the answers experience +
+ + + +
+
+ Type +
+
+ +SearchParams + + +
+
+ + + + + + + + + + + + + +

findSimpleFiltersWithFieldId(persistedFilter, fieldId) → {Array.<Filter>}

+ + + + + + +
+ Given a filter, return an array of all it's descendants, including itself, that +filter on the given fieldId. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
persistedFilter + + +Filter + + + +
fieldId + + +string + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Array.<Filter> + + +
+
+ + + + + + + + + + + + + +

flag(phrase, pluralForm, count, context, interpolationValues) → {string}

+ + + + + + +
+ Any calls of this method will be removed during a preprocessing step during SDK +bundling. + +To support cases where someone may want to bundle without using our +bundling tasks, this function attempts to return the same-language interpolated +and pluralized value based on the information given. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
phrase + + +string + + + +
pluralForm + + +string + + + +
count + + +string +| + +number + + + +
context + + +string + + + +
interpolationValues + + +Object + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +string + + +
+
+ + + + + + + + + + + + + +

flattenFilterNodes(filterNodes) → {Array.<SimpleFilterNode>}

+ + + + + + +
+ Flattens an array of FilterNodes into an array +of their constituent leaf SimpleFilterNodes. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
filterNodes + + +Array.<FilterNode> + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Array.<SimpleFilterNode> + + +
+
+ + + + + + + + + + + + + +

fromCore(appliedFilter)

+ + + + + + +
+ Constructs an SDK AppliedQueryFilter from an answers-core AppliedQueryFilter +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
appliedFilter + + +AppliedQueryFilter + + + + from answers-core
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ AppliedQueryFilter +
+ + + + + + + + + + + + + + + +

fromCore(highlightedFields) → {AppliedHighlightedFields}

+ + + + + + +
+ Constructs an AppliedHighlightedFields object from an answers-core HighlightedField +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
highlightedFields + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +AppliedHighlightedFields + + +
+
+ + + + + + + + + + + + + +

fromData(data)

+ + + + + + +
+ Creating an analytics event from raw data. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
data + + +Object + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

get(key) → {*}

+ + + + + + +
+ Get the current state for the provided key +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
key + + +string + + + + The storage key to get
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ The state for the provided key, undefined if key doesn't exist +
+ + + +
+
+ Type +
+
+ +* + + +
+
+ + + + + + + + + + + + + +

getAll() → {Map.<string, *>}

+ + + + + + +
+ Get the current state for all key/value pairs in storage +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ mapping from key to value representing the current state +
+ + + +
+
+ Type +
+
+ +Map.<string, *> + + +
+
+ + + + + + + + + + + + + +

getAnalyticsUrl(env, conversionTrackingEnabled)

+ + + + + + +
+ Returns the base url for the analytics backend in the desired environment. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
env + + +string + + + + The desired environment.
conversionTrackingEnabled + + +boolean + + + + If conversion tracking has been opted into.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

getCachedLiveApiUrl(env)

+ + + + + + +
+ Returns the base url for the live api backend in the desired environment. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
env + + +string + + + + The desired environment.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

getContainerClass(searchState) → {string}

+ + + + + + +
+ Returns a CSS class for the input searchState +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
searchState + + +SearchState + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +string + + +
+
+ + + + + + + + + + + + + +

getCurrentStateUrlMerged() → {string}

+ + + + + + +
+ Returns the url representing the current persisted state, merged +with any additional query params currently in the url. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +string + + +
+
+ + + + + + + + + + + + + +

getKnowledgeApiUrl(env)

+ + + + + + +
+ Returns the base url for the knowledge api backend in the desired environment. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
env + + +string + + + + The desired environment.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

getLiveApiUrl(env)

+ + + + + + +
+ Returns the base url for the live api backend in the desired environment. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
env + + +string + + + + The desired environment.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

getPersistedRangeFilterContents(persistedFilter, fieldId) → {Object}

+ + + + + + +
+ Finds a persisted range filter for the given fieldId, and returns its contents. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
persistedFilter + + +Filter + + + +
fieldId + + +string + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Object + + +
+
+ + + + + + + + + + + + + +

getUrlParams(urlParams) → {SearchParams}

+ + + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
urlParams + + +string + + + + a query param string like "query=hi&otherParam=hello"
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +SearchParams + + +
+
+ + + + + + + + + + + + + +

getUrlWithCurrentState() → {string}

+ + + + + + +
+ Returns the query parameters to encode the current state +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ The query parameters for a page link with the + current state encoded + e.g. query=all&context=%7Bkey:'hello'%7D +
+ + + +
+
+ Type +
+
+ +string + + +
+
+ + + + + + + + + + + + + +

groupArray(arr, keyFunc, valueFunc, intitial) → {Object}

+ + + + + + +
+ Groups an array into an object using a given key and value function, and an initial object +to add to. By default the key and value functions will not perform any transformations +on the array elements. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
arr + + +Array.<any> + + + + array to be grouped
keyFunc + + +function + + + + function that evaluates what key to give an array element.
valueFunc + + +function + + + + function that evaluates what value to give an array element.
intitial + + +Object + + + + the initial object to add to, defaulting to {}
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Object + + +
+
+ + + + + + + + + + + + + +

has(key) → {boolean}

+ + + + + + +
+ Whether the specified key exists or not +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
key + + +string + + + + the storage key
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +boolean + + +
+
+ + + + + + + + + + + + + +

init(url) → {Storage}

+ + + + + + +
+ Decodes the initial state from the query params. This could be a +direct mapping from query param to storage keys in the storage or +could fetch a sessionId from some backend +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
url + + +string + + + + The starting URL
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Storage + + +
+
+ + + + + + + + + + + + + +

markup()

+ + + + + + +
+ returns the svg markup +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

mergeTabOrder(tabOrder, otherTabOrder) → {Array.<string>}

+ + + + + + +
+ mergeTabOrder merges two arrays into one +by appending additional tabs to the end of the original array +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
tabOrder + + +Array.<string> + + + + Tab order provided by the server
otherTabOrder + + +Array.<string> + + + + Tab order provided by configuration
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Array.<string> + + +
+
+ + + + + + + + + + + + + +

nestValue(value, keys) → {Object}

+ + + + + + +
+ Nest a value inside an object whose structure is defined by an array of keys + +Example: if `value` is 'Hello, world!', and `keys` is ['a', 'b'], +the function will return the object: + +{ + a: { + b: 'Hello, world!' + } +} +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
value + + +* + + + +
keys + + +Array.<string> + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Object + + +
+
+ + + + + + + + + + + + + +

process(translations, interpolationParams, count, language, escapeExpression) → {string}

+ + + + + + +
+ Processes a translation which includes performing interpolation, pluralization, or +both +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
translations + + +string +| + +Object + + + + The translation, or an object containing +translated plural forms
interpolationParams + + +Object + + + + Params to use during interpolation
count + + +number + + + + The count associated with the pluralization
language + + +string + + + + The langauge associated with the pluralization
escapeExpression + + +string + + + + A function which escapes HTML in the passed string
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ The translation with any interpolation or pluralization applied +
+ + + +
+
+ Type +
+
+ +string + + +
+
+ + + + + + + + + + + + + +

pruneFilterNodes(filterNodes, hiddenFields) → {Array.<FilterNode>}

+ + + + + + +
+ Returns the given array of FilterNodes, +removing FilterNodes that are empty or have a field id listed as a hidden. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
filterNodes + + +Array.<FilterNode> + + + +
hiddenFields + + +Array.<string> + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Array.<FilterNode> + + +
+
+ + + + + + + + + + + + + +

pushStateToHistory()

+ + + + + + +
+ Adds all entries of the persistent storage to the URL. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

registerListener(listener)

+ + + + + + +
+ Adds a listener to the given module for a given event +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
listener + + +StorageListener + + + + the listener to add
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

registerMiddleware(middlewareFunction)

+ + + + + + +
+ Register a middleware, to be called before searches are run. +Middleware must return a Promise if they are async. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
middlewareFunction + + +function + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

removeListener(listener)

+ + + + + + +
+ Removes a given listener from the set of listeners +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
listener + + +StorageListener + + + + the listener to remove
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

removeParamsWithPrefixes(params, prefixes) → {SearchParams}

+ + + + + + +
+ Creates a copy of the provided SearchParams, with the specified +attributes filtered out +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
params + + +SearchParams + + + + The parameters to remove from
prefixes + + +Array.<string> + + + + The prefixes of parameters to remove
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ A new instance of SearchParams without entries with + keys that start with the given prefixes +
+ + + +
+
+ Type +
+
+ +SearchParams + + +
+
+ + + + + + + + + + + + + +

replaceHistoryWithState()

+ + + + + + +
+ Adds all entries of the persistent storage to the URL, +replacing the current history state. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

replaceUrlParams(url, params) → {string}

+ + + + + + +
+ Returns the passed in url with the passed in params appended as query params +Note: query parameters in the url are stripped, you should include those query parameters +in `params` if you want to keep them +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
url + + +string + + + +
params + + +SearchParams + + + + to add to the url
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +string + + +
+
+ + + + + + + + + + + + + +

report(err)

+ + + + + + +
+ Reports an error to backend servers for logging +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
err + + +AnswersBaseError + + + + The error to be reported
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

report(event) → {boolean}

+ + + + + + +
+ Report an analytics event +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +AnalyticsEvent + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ whether the event was successfully reported +
+ + + +
+
+ Type +
+
+ +boolean + + +
+
+ + + + + + + + + + + + + +

report()

+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

report()

+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

set(key, data)

+ + + + + + +
+ Set the data in storage with the given key to the provided +data, completely overwriting any existing data. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
key + + +string + + + + The storage key to set
data + + +* + + + + The data to set
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

setConversionTrackingEnabled(isEnabled)

+ + + + + + +
+ Enable or disable conversion tracking +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
isEnabled + + +boolean + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

setConversionTrackingEnabled()

+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

setWithPersist(key, data)

+ + + + + + +
+ Updates the storage with a new entry of [key, data]. The entry +is not added to the URL until the history is updated. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
key + + +string + + + + The storage key to set
data + + +* + + + + The data to set
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

toApiEvent()

+ + + + + + +
+ Return the event in the api format, typically for reporting to the api +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

transformAutoCompleteResponse(response) → {AutoCompleteData}

+ + + + + + +
+ Converts a universal or vertical autocomplete response from the +core library into an object that the SDK understands. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
response + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +AutoCompleteData + + +
+
+ + + + + + + + + + + + + +

transformFilterSearchResponse(response) → {AutoCompleteData}

+ + + + + + +
+ Converts a filter search response from the +core library into an object that the SDK understands. +
+ + + + + + + + + +
Parameters:
-

Methods

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
response + +
+ + + + + + +
+ -

addOptions(options)

+ + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +AutoCompleteData + + +
+
+ + + + + + + + + + + + + +

truncate(str, limit, trailing, sep) → {string}

@@ -112,7 +8702,7 @@

addOptions<
- Adds the provided options to the event + Truncates strings to 250 characters, attempting to preserve whole words
@@ -148,13 +8738,13 @@

Parameters:
- options + str -object +string @@ -164,7 +8754,76 @@
Parameters:
- Additional options for the event + the string to truncate + + + + + + + limit + + + + + +Number + + + + + + + + + + the maximum character length to return + + + + + + + trailing + + + + + +string + + + + + + + + + + a trailing string to denote truncation, e.g. '...' + + + + + + + sep + + + + + +string + + + + + + + + + + the word separator @@ -205,7 +8864,7 @@
Parameters:
Source:
@@ -230,6 +8889,24 @@
Parameters:
+
Returns:
+ + + + +
+
+ Type +
+
+ +string + + +
+
+ + @@ -241,7 +8918,7 @@
Parameters:
-

toApiEvent()

+

urlWithoutQueryParamsAndHash(url) → {string}

@@ -249,7 +8926,7 @@

toApiEvent<
- Return the event in the api format, typically for reporting to the api + Returns the given url without query params and hashes
@@ -260,6 +8937,55 @@

toApiEvent< +

Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
url + + +string + + + + Full url e.g. https://yext.com/?query=hello#Footer
+ + @@ -293,7 +9019,7 @@

toApiEvent<
Source:
@@ -318,6 +9044,28 @@

toApiEvent< +

Returns:
+ + +
+ Url without query params and hashes e.g. https://yext.com/ +
+ + + +
+
+ Type +
+
+ +string + + +
+
+ + @@ -339,13 +9087,13 @@

toApiEvent<
- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/index.html b/docs/index.html index 5746beebf..beceb6bad 100644 --- a/docs/index.html +++ b/docs/index.html @@ -43,310 +43,1126 @@

-

Answers

-

Answers Javascript API Library.

+

Answers Search UI

Outline:

    -
  1. Install / Setup +
  2. Install / Setup
  3. +
  4. ANSWERS.init Configuration Options
  5. Component Usage
  6. -
  7. Types of Components +
  8. Types of Built-in Components +
  9. +
  10. Customizing Components + +
  11. +
  12. Template Helpers
  13. Analytics
  14. +
  15. Rich Text Formatting
  16. +
  17. Processing Translations
  18. +
  19. Performance Metrics

Install and Setup

-

To include the answers base CSS (optional).

+

The Answers Javascript API Library does not need to be installed locally. Instead, it can be used +with script tags on a webpage. The instructions below explain how to do this; they will walk you through +adding the Answers stylesheet, JS library, and an intialization script to an HTML page. +After doing this, you can view your page in the browser.

+

Include the Answers CSS

<link rel="stylesheet" type="text/css" href="https://assets.sitescdn.net/answers/answers.css">
 
-

Adding the Javascript library

-
<script src="https://assets.sitescdn.net/answers/answers.min.js" onload="ANSWERS.domReady(initAnswers)" async></script>
+

Add the Javascript library and placeholder elements for Answers components.

+
<script src="https://assets.sitescdn.net/answers/answers.min.js" onload="ANSWERS.domReady(initAnswers)" defer async></script>
+
+<div id="SearchBarContainer"></div>
+<div id="UniversalResultsContainer"></div>
 
+

Add an initialization script with an apiKey, experienceKey and onReady function. In the example below, we've initialized two +basic components: SearchBar and UniversalResults.

function initAnswers() {
   ANSWERS.init({
-    apiKey: '<API_KEY_HERE>',
-    answersKey: '<ANSWERS_KEY_HERE>',
+    apiKey: '<API_KEY_HERE>', // See [1]
+    experienceKey: '<EXPERIENCE_KEY_HERE>',
     onReady: function() {
-      // Component creation logic here
-    })
+      ANSWERS.addComponent('SearchBar', {
+        container: '#SearchBarContainer',
+      });
+
+      ANSWERS.addComponent('UniversalResults', {
+        container: '#UniversalResultsContainer',
+      });
+    }
   })
 }
 
-

Configuration Options

-

Below is a list of configuration options that can be used during initialization.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
optiontypedescriptionrequired
apiKeystringYour API keyrequired
answersKeystringThe key used for your answers projectrequired
onReadyfunctionInvoked when the Answers component library is loaded/readyrequired
useTemplatesbooleandefault: true. If false, don't fetch pre-made templates. Only use this if you plan to implement custom renders for every component!not required
templateUrlstringUse precompiled template hosted by younot required
templateBundleobjectProvide the precompiled templatesnot required
localestringThe locale of the configuration. The locale will affect how queries are interpreted and the results returned. The default locale value is 'en'.not required
-

Template Helpers

-

When using handlebars templates, Answers ships with a bunch of pre-built template helpers that you can use. You can learn more about them here.

-

If you want to register custom template helpers to the handlebars render, you can do so like this:

-
ANSWERS.registerHelper('noop', function(options) {
-  return options.fn(this);
-})
+

ANSWERS.init() returns a promise which optionally allows components to be added using promise syntax

+
function initAnswers() {
+  ANSWERS.init({
+    apiKey: '<API_KEY_HERE>', // See [1]
+    experienceKey: '<EXPERIENCE_KEY_HERE>',
+  }).then(function() {
+    ANSWERS.addComponent('SearchBar', {
+      container: '#SearchBarContainer',
+    });
+
+    ANSWERS.addComponent('UniversalResults', {
+      container: '#UniversalResultsContainer',
+    });
+  });
+}
+
+

[1] Learn more about getting your API key.

+

ANSWERS.init Configuration Options

+

The configuration provided here is configuration that is shared across components.

+
function initAnswers() {
+  ANSWERS.init({
+    // Required, your Yext Answers API key
+    apiKey: '<API_KEY_HERE>',
+    // Required, the key used for your Answers experience
+    experienceKey: '<EXPERIENCE_KEY_HERE>',
+    // Optional, initialize components here, invoked when the Answers component library is loaded/ready.
+    //    If components are not added here, they can also be added when the init promise resolves
+    onReady: function() {},
+    // Optional*, Yext businessId, *required to send analytics events
+    businessId: 'businessId',
+    // Optional, if false, the library will not fetch pre-made templates. Only use change this to false if you provide a
+    //    template bundle in the `templateBundle` config option or implement custom renders for every component
+    useTemplates: true,
+    // Optional, additional templates to register with the renderer
+    templateBundle: {},
+    // Optional, provide configuration for each vertical that is shared across components, see Vertical Pages Configuration below
+    verticalPages: [],
+    // Optional, search specific settings, see Search Configuration below
+    search: {},
+    // Optional, vertical no results settings, see Vertical No Results below
+    noResults: {},
+    // Optional, the locale will affect how queries are interpreted and the results returned. Defaults to 'en'.
+    locale: 'en',
+    // Optional, the Answers Experience version to use for api requests
+    experienceVersion: 'PRODUCTION',
+    // Optional, prints full Answers error details when set to `true`. Defaults to false.
+    debug: false,
+    // Optional, If true, the search session is tracked. If false, there is no tracking. Defaults to true.
+    sessionTrackingEnabled: true,
+    // Optional, invoked when the state of any component changes
+    onStateChange: function() {},
+    // Optional, analytics callback after a vertical search, see onVerticalSearch Configuration for additional details
+    onVerticalSearch: function() {},
+    // Optional, analytics callback after a universal search, see onUniversalSearch Configuration for additional details
+    onUniversalSearch: function() {},
+    // Optional, opt-out of automatic css variable resolution on init for legacy browsers
+    disableCssVariablesPonyfill: false,
+    // Optional, the analytics key describing the Answers integration type. Accepts 'STANDARD' or 'OVERLAY', defaults to 'STANDARD'
+    querySource: 'STANDARD',
+  })
+}
+
+

Vertical Pages Configuration

+

Below is a list of configuration options related to vertical pages in navigation and no results, used in the base configuration above.

+
verticalPages: [
+  {
+    // Required, the label for this page
+    label: 'Home',
+    // Required, the link to this page
+    url: './index.html',
+    // Optional*, the verticalKey, *required for vertical pages (must omit this property for universal)
+    verticalKey: 'locations',
+    // Optional, the icon name to use in no results, defaults to no icon
+    icon: 'star',
+    // Optional, the URL icon to use in no results, defaults to no icon
+    iconUrl: '',
+    // Optional, if true, will show this page first in the Navigation Component, defaults to false
+    isFirst: false,
+    // Optional, if true, will add a special styling to this page in the Navigation Component, defaults to false
+    isActive: false,
+    // Optional, if true, hide this tab in the Navigation Component, defaults to false
+    hideInNavigation: false,
+  },
+  ...
+]
+
+

Search Configuration

+

Below is a list of configuration options related to search, used in the base configuration above.

+
    search: {
+      // Optional, the vertical key to use for searches
+      verticalKey: 'verticalKey',
+      // Optional, the number of results to display per page, defaults to 20. Maximum is 50.
+      limit: '20',
+      // Optional, Vertical Pages only, a default search to use on page load when the user hasn't provided a query
+      defaultInitialSearch: 'What is Yext Answers?',
+    },
+
+

Vertical No Results Configuration

+

Below is a list of configuration options related to no results on Vertical Pages, used in the base configuration above.

+
    noResults: {
+      // Optional, whether to display all results for the Vertical when a query has no results, defaults to false
+      displayAllResults: false,
+      // Optional, a custom template for the no results card
+      template: '',
+    },
+
+

onVerticalSearch Configuration

+

The onVerticalSearch Configuration is a function, used in the base configuration above.

+

It allows you to send an analytics event each time a search is run on a Vertical page. This function should take in one parameter, searchParams, which contains information about the search, and return the desired analytics event.

+

Like all Answers Javascript API Library analytics, this will only work if there is a businessId in the ANSWERS.init.

+

The search information exposed in searchParams is shown below.

+
function (searchParams) => {
+    /**
+     * Vertical key used for the search.
+     * @type {string}
+     */
+    const verticalKey = searchParams.verticalKey;
+
+    /**
+     * The string being searched for.
+     * @type {string}
+     */
+    const queryString = searchParams.queryString;
+
+    /**
+     * The total number of results found.
+     * @type {number}
+     */
+    const resultsCount = searchParams.resultsCount;
+
+    /**
+     * Either 'normal' or 'no-results'.
+     * @type {string}
+     */
+    const resultsContext = searchParams.resultsContext;
+
+    let analyticsEvent = new ANSWERS.AnalyticsEvent('ANALYTICS_EVENT_TYPE');
+    analyticsEvent.addOptions({
+      label: 'Sample analytics event',
+      searcher: 'VERTICAL',
+      query: queryString,
+      resultsCount: resultsCount,
+      resultsContext: resultsContext,
+    });
+    return analyticsEvent;
+  },
+}),
+
+

onUniversalSearch Configuration

+

The onUniversalSearch Configuration is a function, used in the base configuration above.

+

It allows you to send an analytics event each time a search is run +on a Universal page. This function should take in one parameter, searchParams, which contains information about the +search, and return the desired analytics event.

+

Like all Answers Javascript API Library analytics, this will only work if there is a businessId in the ANSWERS.init.

+

The search information exposed in searchParams is shown below.

+
function (searchParams) => {
+    /**
+     * The string being searched for.
+     * @type {string}
+     */
+    const queryString = searchParams.queryString;
+
+    /**
+     * The total number of results found.
+     * @type {number}
+     */
+    const sectionsCount = searchParams.sectionsCount;
+
+    /**
+     * A map containing entries of the form:
+     * { totalResultsCount: 150, displayedResultsCount: 10}
+     * for each returned vertical. The totalResultsCount indicates how many results
+     * are present in the vertical. The displayResultsCount indicates how many of
+     * those results are actually displayed.
+     * @type {Object<string,Object>}
+     */
+    const resultsCountByVertical = searchParams.resultsCountByVertical;
+
+    let analyticsEvent = new ANSWERS.AnalyticsEvent('ANALYTICS_EVENT_TYPE');
+    analyticsEvent.addOptions({
+      type: 'ANALYTICS_EVENT_TYPE',
+      label: 'Sample analytics event',
+      searcher: 'UNIVERSAL',
+      query: queryString
+      sectionsCount: sectionsCount,
+    });
+    return analyticsEvent;
+  },
+}),
 
-

You can learn more about the interface for registering helpers by taking a look at the Handlebars Block Helpers documentation.

Component Usage

The Answers Component Library exposes an easy to use interface for adding and customizing various types of UI components on your page.

-

Every component requires a containing HTML element.

+

What is a Component?

+

At a high level, components are the individual pieces of an Answers page. The Answers Javascript API Library comes with many types of components. Each component is an independent, reusable piece of code. A component fills an HTML element container that the implementer provides on the page. Components are updated from their config, the config from the ANSWERS.init, and potentially an API response.

+

Each type of Component has its own custom configurations. Additionally, all components share the base configuration options defined above. We will provide a brief description below of what each component does, along with describing how it can be configured.

Base Component Configuration

Every component has the same base configuration options.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
optiontypedescriptionrequired
namestringa unique name, if using multiple components of the same typeoptional
containerstringthe CSS selector to append the component.required
classstringa custom class to apply to the componentnot required
templatestringoverride internal handlebars templatenot required
renderfunctionoverride render function. data providednot required
transformDatafunctionA hook for transforming data before it gets sent to rendernot required
onMountfunctioninvoked when the HTML is mounted to the DOMnot required
-

Adding a Component

+
  {
+    // Required, the selector for the container element where the component will be injected
+    container: 'container',
+    // Optional, a unique name for the component
+    name: 'name',
+    // Optional, an additional, custom HTML classname for the component. The component will also 
+    // have a classname of 'yxt-Answers-component' applied.
+    class: 'class',
+    // Optional, handlebars template or HTML to override built-in handlebars template for the component
+    template: 'template',
+    // Optional, override render function
+    render: function(data) {},
+    // Optional, a hook for transforming data before it gets sent to render
+    transformData: function(data) {},
+    // Optional, invoked when the HTML is mounted to the DOM, this will not override any built-in onMount function for a component
+    onMount: function(data) {},
+    // Optional, invoked when the HTML is mounted to the DOM, this will override any built-in onMount function for a component
+    onMountOverride: function(data) {},
+    // Optional, additional properties to send with every analytics event
+    analyticsOptions: {},
+  }
+
+

Adding a Component to Your Page

Adding a component to your page is super easy! -You can add many different types of components to your page. +You can add many different types of components to your page. Each component supports the base configuration options above, as well as their own unique configurations.

To start, every component requires an HTML container.

<div class="search-container"></div>
 
-

Then, you can add a component to your page through the ANSWERS add interface.

-

This is an example of the SearchBar. See Types of Components below.

+

Then, you can add a component to your page through the ANSWERS add interface. You need to call addComponent from onReady.

+

This is an example of the SearchBar. See Types of Built-in Components below.

ANSWERS.addComponent('SearchBar', {
-  container: '.search-container',
-  // -- other options --
+  container: '.search-container'
 })
 
-

Using a Custom Template

-

All component templates are written using handlebars.

-

It's easy to override these templates with your own templates. -Keep in mind, that you must provide valid handlebars syntax here.

-
// Use handlebars syntax to create a template string
-let customTemplate = `<div class="my-search">{{title}}</div>`
-
-ANSWERS.addComponent('SearchBar', {
-  container: '.search-container',
-  template: customTemplate
-})
-
-

Using a Custom Renderer

-

If you want to use a use your own template language (e.g. soy, mustache, groovy, etc), -you should NOT use the template argument. Instead, you can provide a custom render function to the component.

+

Removing Components

+

If you'd like to remove a component and all of its children, you can do it. Simply ANSWERS.removeComponent(<component name>):

ANSWERS.addComponent('SearchBar', {
   container: '.search-container',
-  render: function(data) {
-    // Using native ES6 templates -- but you can replace this with soy,
-    // or any other templating language as long as it returns a string.
-    return `<div class="my-search">${data.title}</div>`
-  }
+  name: 'MySearchBar'
 })
+
+ANSWERS.removeComponent('MySearchBar');
 
-

Custom Data Transforms

-

If you want to mutate the data thats provided to the render/template before it gets rendered, -you can use the transformData hook.

-

All properties and values that you return from here will be accessible from templates.

+

Types of Built-in Components

+

SearchBar Component

+

The SearchBar component is the main entry point for search querying. It provides the input box, where the user +types their query, as well as the autocomplete behavior.

+
<div class="search-query-container"></div>
+
+

If the verticalKey config option is omitted, the SearchBar will perform Universal searches. Universal +searches return results across multiple Verticals; Vertical searches search within one Vertical. Additionally, Universal search and Vertical search provide a different way of auto complete.

ANSWERS.addComponent('SearchBar', {
-  container: '.search-container',
-  transformData: (data) => {
-    // Extend/overide the data object
-    return Object.assign({}, data, {
-      title: data.title.toLowerCase()
-    })
+  // Required, the selector for the container element where the component will be injected
+  container: '.search-query-container',
+  // Required* for Vertical pages, omit for Universal pages
+  verticalKey: '<VERTICAL_KEY>',
+  // Optional, title is not present by default
+  title: 'Search my Brand',
+  // Optional, the initial query string to use for the input box
+  query: 'query',
+  // Optional, defaults to 'Conduct a search'
+  labelText: 'What are you looking for?',
+  // Optional, used for labeling the submit button, also provided to the template
+  submitText: 'Submit',
+  // Optional, used for labeling the clear button, also provided to the template
+  clearText: 'Clear',
+  // Optional, used to specify a different built-in icon for the submit button. Defaults to Animated Magnifying glass when CSS is included.
+  submitIcon: 'iconName',
+  // Optional, a url for a custom icon for the submit button. Defaults to Animated Magnifying glass when CSS is included.
+  customIconUrl: 'path/to/icon',
+  // Optional, the query text to show as the first item for auto complete
+  promptHeader: 'Header',
+  // Optional, no default
+  placeholderText: 'Start typing...',
+  // Optional, auto focuses the search bar. Defaults to false
+  autoFocus: false,
+  // Optional, opens the autocomplete suggestions on page load. Defaults to false. Requires autoFocus to be set to true
+  autocompleteOnLoad: false,
+  // Optional, allows a user to conduct an empty search. Should be set to true if the defaultInitialSearch is "".
+  allowEmptySearch: false,
+  // Optional, defaults to 300ms (0.3 seconds)
+  searchCooldown: 2000,
+  // Optional, asks the user for their geolocation when "near me" intent is detected
+  promptForLocation: true,
+  // Optional, displays an "x" button to clear the current query when true
+  clearButton: true,
+  // Optional, redirect search query to url
+  redirectUrl: 'path/to/url',
+  // Optional, target frame for the redirect url, defaults to current frame. Expects a valid target: "_blank", "_self", "_parent", "_top" or the name of a frame
+  redirectUrlTarget: '_self',
+  // Optional, defaults to native form node within container
+  formSelector: 'form',
+  // Optional, defaults to true. When true, a form is used as the query submission context.
+  // Note that WCAG compliance is not guaranteed if a form is not used as the context.
+  useForm: 'true',
+  // Optional, the input element used for searching and wires up the keyboard interaction
+  inputEl: '.js-yext-query',
+  // Optional, options to pass to the geolocation api, which is used to fetch the user's current location.
+  // https://developer.mozilla.org/en-US/docs/Web/API/PositionOptions
+  geolocationOptions: {
+    // Optional, whether to improve accuracy at the cost of response time and/or power consumption, defaults to false.
+    enableHighAccuracy: false,
+    // Optional, the maximum amount of time (in ms) a geolocation call is allowed to take before defaulting, defaults to 1 second.
+    timeout: 1000,
+    // Optional, the maximum amount of time (in ms) to cache a geolocation call, defaults to 5 minutes.
+    maximumAge: 300000,
   },
-  render: function(data) {
-    // Using native ES6 templates -- but you can replace this with soy,
-    // or any other templating language as long as it returns a string.
-    return `<div class="my-search">${data.title}</div>`
+  // Optional, options for an alert when the geolocation call fails.
+  geolocationTimeoutAlert: {
+    // Optional, whether to display a window.alert() on the page, defaults to false.
+    enabled: false,
+    // Optional, the message in the alert. Defaults to the below
+    message: "We are unable to determine your location"
+  },
+  // Optional, functions invoked when certain events occur
+  customHooks: {
+    // Optional, a callback invoked when the clear search button is clicked
+    onClearSearch: function() {},
+    // Optional, a function invoked when a search is conducted. The search terms are passed in as a string
+    onConductSearch: function(searchTerms) {}
+  },
+  // Optional, options to pass to the autocomplete component
+  autocomplete: {
+    // Optional, boolean used to hide the autocomplete when the search input is empty (even if the
+    // input is focused). Defaults to false.
+    shouldHideOnEmptySearch: false,
+    // Optional, callback invoked when the autocomplete component changes from open to closed.
+    onClose: function() {},
+    // Optional, callback invoked when the autocomplete component changes from closed to open.
+    onOpen: function() {},
   }
 })
 
-

Types of Components

-

Each type of Component has it's own custom configurations. However, all components share the -base configuration options defined above.

-

Navigation Component

-

The Navigation Component adds a dynamic experience to your pages navigation experience. -When using multiple veritcal searches in a universal search, the navigation ordering will be automatically -updated based on the search results.

-
<nav class="navigation-container"></nav>
+

Direct Answer Component

+

This component is for Universal pages only.

+

The Direct Answer Component will render the BEST result, if found, based on the query.

+
<div class="direct-answer-container"></div>
 
-
ANSWERS.addComponent('Navigation', {
-  container: '.navigation-container',
-  tabs: [
+
ANSWERS.addComponent('DirectAnswer', {
+  // Required, the selector for the container element where the component will be injected
+  container: '.direct-answer-container',
+  // Optional, a custom direct answer card to use, which is the default when there are no matching card overrides.
+  // See the Custom Direct Answer Card section below.
+  defaultCard: 'MyCustomDirectAnswerCard',
+  // Optional, the selector for the form used for submitting the feedback
+  formEl: '.js-directAnswer-feedback-form',
+  // Optional, the selector to bind ui interaction to for reporting
+  thumbsUpSelector: '.js-directAnswer-thumbUp',
+  // Optional, the selector to bind ui interaction to for reporting
+  thumbsDownSelector: '.js-directAnswer-thumbDown',
+  // Optional, the display text for the View Details click to action link
+  viewDetailsText: 'View Details',
+  // Optional, the screen reader text for positive feedback on the answer
+   positiveFeedbackSrText: 'This answered my question',
+  // Optional, the screen reader text for negative feedback on the answer
+   negativeFeedbackSrText: 'This did not answer my question',
+  // Optional, the footer text to display on submission of feedback
+  footerTextOnSubmission: 'Thank you for your feedback!',
+  // Optional, specify card types and overrides based on the direct answer type. The first matching cardOverride will be used, otherwise the cardType is used
+  types: {
+    'FEATURED_SNIPPET': {
+      cardType: "documentsearch-standard",
+      cardOverrides: [
+        {
+          fieldName: 'description',
+          entityType: 'ce_menuItem',
+          cardType: 'MenuItemDescriptionDirectAnswer'
+        },
+        {
+          fieldName: 'description',
+          entityType: 'ce_menuItem',
+          fieldType: 'rich_text'
+          cardType: 'MenuItemDescriptionDirectAnswer'
+        }
+      ]
+    },
+    'FIELD_VALUE': {
+      cardType: "allfields-standard",
+      cardOverrides: [
+        {
+          cardType: 'MenuItemDescriptionDirectAnswer',
+          fieldName: 'description',
+          entityType: 'ce_menuItem',
+          fieldType: 'rich_text'
+        }
+      ]
+    }
+  }
+  // DEPRECATED: use the types option instead
+  // Optional, card overrides that allow you to specify a specific direct answers card depending on the fieldName, entityType, and fieldType of the direct answer. The first matching card will be used, otherwise defaultCard will be used.
+  cardOverrides: [
     {
-      label: 'Home',         // The label used for the navigation element
-      url: './index.html',   // The link for the navigation element
-      isFirst: true,         // optional, will always show this item first
-      isActive: true         // optional, will add a special class to the item
+      cardType: 'MenuItemDescriptionDirectAnswer',
+      fieldName: 'description',
+      entityType: 'ce_menuItem',
+      fieldType: 'rich_text'
     },
     {
-      configId: 'locations'  // optional, the vertical search config id
-      label: 'Location'      // The label used for the navigation element
-      url: 'locations.html'  // The link for the navigation element
+      cardType: 'DeliveryHoursDirectAnswer',
+      fieldName: 'c_deliveryHours'
     },
     {
-      configId: 'employees'  // optional, the vertical search config id
-      label: 'Employees'     // The label used for the navigation element
-      url: 'employees.html'  // The link for the navigation element
+      cardType: 'PhoneDirectAnswer',
+      fieldType: 'phone'
     }
   ]
 })
 
-

SearchBar Component

-

The SearchBar component is the main entry point for search querying. It provides the input box, where the user -types their query, as well as the autocomplete behavior.

-
<div class="search-query-container"></div>
+

Creating a Custom Direct Answer Card

+

You can customize the look and behavior of your Direct Answer by creating a custom Direct Answer card.

+

A custom Direct Answer card is given the same data as the built-in card. +That data will look something like the below:

+
{
+  type: "FIELD_VALUE",
+  answer: {
+    entityName: "Entity Name",
+    fieldName: "Phone Number",
+    fieldApiName: "mainPhone",
+    value: "+11234567890",
+    fieldType: "phone" 
+  },
+  relatedItem: { 
+    verticalConfigId: 'people',
+    data: { 
+      id: "Employee-2116",
+      type: "ce_person",
+      fieldValues: {
+        description: "This is the description field.",
+        name: "First Last",
+        firstName: "First",
+        lastName: "Last",
+        mainPhone: "+1234567890",
+      }
+    }
+  }
+}
 
-

There are two types of search experiences. Universal Search and Vertical Search. -Each provide a different way of auto complete.

-

For Universal Search:

-
ANSWERS.addComponent('SearchBar', {
-  container: '.search-query-container',
-  title: 'Search my Brand',                // optional, defaults to 'Answers'
-  searchText: 'What are you looking for?', // optional, defaults to 'What are you interested in?'
-  autoFocus: true,                          // optional, defaults to false
-  redirectUrl: 'path/to/url'                // optional, redirect search query to url
+

A custom Direct Answer card needs a corresponding template. +This can be added either inline by changing the component's constructor to:

+
    constructor(config, systemConfig) {
+      super(config, systemConfig);
+      this.setTemplate(`<div> your template here </div>`)
+    }
+
+

Or by including a custom template bundle, and adding:

+
  static defaultTemplateName () {
+    return 'CustomDirectAnswerTemplate';
+  }
+
+

Where 'CustomDirectAnswerTemplate' is the name the template is registered under.

+

We will use the following template for our example card.

+
  <div class="customDirectAnswer">
+    <div class="customDirectAnswer-type">
+      {{type}}
+    </div>
+    <div class="customDirectAnswer-value">
+      {{#each customValue}}
+      {{#if url}}
+        {{> valueLink }}
+      {{else}}
+        {{{this}}}
+      {{/if}}
+      {{/each}}
+    </div>
+    {{> feedback}}
+  </div>
+
+  {{#*inline 'feedback'}}
+  <span class="customDirectAnswer-thumbsUpIcon js-customDirectAnswer-thumbsUpIcon"
+    data-component="IconComponent"
+    data-opts='{"iconName": "thumb"}'
+  ></span>
+  <span class="customDirectAnswer-thumbsDownIcon js-customDirectAnswer-thumbsDownIcon"
+    data-component="IconComponent"
+    data-opts='{"iconName": "thumb"}'
+  ></span>
+  {{/inline}}
+
+  {{#*inline 'valueLink'}}
+  <a class="customDirectAnswer-fieldValueLink" href="{{{url}}}"
+    {{#if @root/eventType}}data-eventtype="{{@root/eventType}}"{{/if}}
+    {{#if @root/eventOptions}}data-eventoptions='{{{ json @root/eventOptions }}}'{{/if}}>
+    {{{displayText}}}
+  </a>
+  {{/inline}}
+
+

This specific example needs some css to flip the thumbs up icon the right way.

+
  .customDirectAnswer-thumbsUpIcon svg {
+    transform: rotate(180deg);
+  }
+
+

This is the javascript class for our custom Direct Answer card. +It applies custom formatting to the Direct Answer, registers analytics events +to the thumbs up/down icons, and passes custom event options into the template.

+
  class CustomDirectAnswerClass extends ANSWERS.Component {
+    constructor(config, systemConfig) {
+      // If you need to override the constructor, make sure to call super(config, systemConfig) first.
+      super(config, systemConfig);
+
+      // For simplicity's sake, we set this card's template using setTemplate(), as opposed to
+      // a custom template bundle.
+      this.setTemplate(`<div> your template here </div>`)
+    }
+
+    /**
+     * setState() lets you pass variables directly into your template.
+     * Here, data is the directAnswer data from the query.
+     * Below, we pass through a custom direct answers value, customValue.
+     * @param {Object} data
+     * @returns {Object}
+     */ 
+    setState(data) {
+      const { type, answer, relatedItem } = data;
+      const associatedEntityId = data.relatedItem && data.relatedItem.data && data.relatedItem.data.id;
+      const verticalConfigId = data.relatedItem && data.relatedItem.verticalConfigId;
+      return super.setState({
+        ...data,
+        customValue: this.getCustomValue(answer),
+        eventType: 'CUSTOM_EVENT',
+        eventOptions: {
+          searcher: 'UNIVERSAL',
+          verticalConfigId: verticalConfigId,
+          entityId: associatedEntityId,
+        }
+      });
+    }
+
+    /**
+     * onMount() lets you register event listeners. Here, we register the thumbs up and thumbs
+     * down buttons to fire an analytics event on click.
+     */ 
+    onMount() {
+      const thumbsUpIcon = this._container.querySelector('.js-customDirectAnswer-thumbsUpIcon');
+      const thumbsDownIcon = this._container.querySelector('.js-customDirectAnswer-thumbsDownIcon');
+      thumbsUpIcon.addEventListener('click', () => this.reportQuality(true));
+      thumbsDownIcon.addEventListener('click', () => this.reportQuality(false));
+    }
+
+    /**
+     * reportQuality() sends an analytics event (either THUMBS_UP or THUMBS_DOWN).
+     * @param {boolean} isGood true if the answer is what you were looking for
+     */
+    reportQuality(isGood) {
+      const eventType = isGood === true ? 'THUMBS_UP' : 'THUMBS_DOWN';
+      const event = new ANSWERS.AnalyticsEvent(eventType).addOptions({
+        directAnswer: true
+      });
+      this.analyticsReporter.report(event);
+    }
+
+    /**
+     * Formats a Direct Answer value based on its fieldType.
+     * @param {Object} answer the answer property in the directAnswer model
+     * @returns {string}
+     */ 
+    formatValue(answer) {
+      const { fieldType, value } = answer;
+      switch (fieldType) {
+        case 'phone':
+          return {
+              url: 'http://myCustomWebsite.com/?mainPhone=' + value,
+              displayText: value,
+            };
+        case 'rich_text':
+          return ANSWERS.formatRichText(value);
+        case 'single_line_text':
+        case 'multi_line_text':
+        default:
+          return value;
+      }
+    }
+
+    /**
+     * Computes a custom Direct Answer. If answer.value is an array, this method
+     * formats every value in the array and returns it, otherwise it just formats the single
+     * given value.
+     * @param {Object} answer
+     * @returns {Array<string>}
+     */ 
+    getCustomValue(answer) {
+      if (Array.isArray(answer.value)) {
+        return answer.value.map(value => this.formatValue(answer))
+      } else {
+        return [ this.formatValue(answer) ];
+      }
+    }
+
+    /**
+     * The name of your custom direct answer card. THIS is the value you will use in any config,
+     * such as defaultCard, when you want to specify this custom Direct Answer card.
+     * @returns {string}
+     */
+    static get type() {
+      return 'MyCustomDirectAnswerCard';
+    }
+  }
+
+  // Don't forget to register your Direct Answer card within the SDK. Otherwise the SDK won't recognize your card name!
+  ANSWERS.registerComponentType(CustomDirectAnswerClass);
+
+

Universal Results Component

+

The Universal Results component will render the results of a query, +across all configured verticals, with one section per vertical.

+
<div class="universal-results-container"></div>
+
+
ANSWERS.addComponent('UniversalResults', {
+  // Required, the selector for the container element where the component will be injected
+  container: '.universal-results-container',
+  // Settings for the applied filters bar in the results header. These settings can be overriden in the
+  // "config" option below on a per-vertical basis.
+  appliedFilters: {
+    // If true, show any applied filters that were applied to the universal search. Defaults to true
+    show: true,
+    // If appliedFilters.show is true, whether to display the field name of an applied filter, e.g. "Location: Virginia" vs just "Virginia". Defaults to false.
+    showFieldNames: false,
+    // If appliedFilters.show is true, this is list of filters that should not be displayed.
+    // By default, builtin.entityType will be hidden
+    hiddenFields: ['builtin.entityType'],
+    // The character that separates the count of results (e.g. “1-6”) from the applied filter bar. Defaults to '|'
+    resultsCountSeparator: '|',
+    // Whether to display the change filters link in universal results. Defaults to false.
+    showChangeFilters: false,
+    // The text for the change filters link. Defaults to 'change filters'.
+    changeFiltersText: 'change filters',
+    // The character that separates each field (and its associated filters) within the applied filter bar. Defaults to '|'
+    delimiter: '|',
+    // The aria-label given to the applied filters bar. Defaults to 'Filters applied to this search:'.
+    labelText: 'Filters applied to this search:',
+  },
+  // Optional, configuration for each vertical's results
+  config: {
+    people: { // The verticalKey
+      card: {
+        // Configuration for the cards in this vertical, see Cards
+      },
+      // Optional: A custom handlebars template for this section
+      template: '<div> Custom section template </div>',
+      // The title of the vertical
+      // Defaults to the vertical key, in this example 'people'
+      title: 'People',
+      // Icon to display to the left of the title. Must be one of our built-in icons, defaults to 'star'
+      icon: 'star',
+      // The url for both the viewMore link and the change-filters link. Defaults to '{{VERTICAL_KEY}}.html',
+      // in this case that is 'people.html'
+      url: 'people.html',
+      // Whether to display a view more link. Defaults to true
+      viewMore: true,
+      // The text for the view more link, if viewMore is true. Defaults to 'View More'
+      viewMoreLabel: 'View More!',
+      // Config for the applied filters bar in the results header.
+      appliedFilters: {
+        // Same as appliedFilters settings above. Settings specified here will override any top level settings.
+      },
+      // If true, display the count of results at the very top of the results. Defaults to false.
+      showResultCount: true,
+      // If true, display the total number of results. Defaults to true
+      // Optional, whether to use the AccordionResults component instead of VerticalResults for this vertical
+      useAccordion: false,
+      // Optional, whether to include a map with this vertical's results, defaults to false
+      includeMap: true,
+      // Optional*, if includeMap is true, this is required
+      mapConfig: {
+        // Required, either 'mapbox' or 'google', not case sensitive
+        mapProvider: 'google',
+        // Required, API key for the map provider
+        apiKey: '<<< enter your api key here >>>',
+        // ... Optional, any other config for the Map Component, find more info in the section "Map Component"
+      },
+      // Optional, override the render function for each result in this vertical
+      renderItem: function(data) {},
+      // Optional, override the handlebars template for each item in this vertical
+      itemTemplate: `my item {{name}}`,
+      // DEPRECATED, please use viewMoreLabel instead. viewAllText is a synonym for viewMoreLabel, where viewMoreLabel takes precedence over viewAllText. Defaults to 'View More'.
+      viewAllText: 'View All Results For Vertical'
+    }
+  },
+  // Optional, override the render function for each item in the result list
+  renderItem: function(data) {},
+  // Optional, override the handlebars template for each item in the result list
+  itemTemplate: `my item {{name}}`,
 })
 
-

For Vertical Search:

-
ANSWERS.addComponent('SearchBar', {
-  container: '.search-query-container',
-  verticalKey: '<VERTICAL_KEY>',      // required
-  barKey: '<BAR_KEY>'                 // optional
+

Vertical Results Component

+

The Vertical Results component shares all the same configurations from Universal Results, but you don't need to specifiy a config or context. You may limit the number of search results returned, with a maximum of 50.

+

You define all the options at the top level object.

+
<div class="vertical-results-container"></div>
+
+
ANSWERS.addComponent('VerticalResults', {
+  // Required, the selector for the container element where the component will be injected
+  container: '.vertical-results-container',
+  // Optional, function to give each result item custom rendering
+  renderItem: () => {},
+  // Optional, string to give custom template to result item
+  itemTemplate: `<div> Custom template </div>`,
+  // Optional, set a max number of columns to display at the widest breakpoint. Possible values are 1, 2, 3 or 4, defaults to 1
+  maxNumberOfColumns: 1,
+  // Optional, a modifier that will be appended to a class on the results list like this `yxt-Results--{modifier}`
+  modifier: '',
+  // Optional, whether to hide the default results header that VerticalResults provides. Defaults to false.
+  hideResultsHeader: false,
+  // Optional, the card used to display each individual result, see the Cards section for more details,
+  card: {
+    // Optional, The type of card, built-in types are: 'Standard', 'Accordion', and 'Legacy'. Defaults to 'Standard'
+    cardType: 'Standard',
+    // Optional, see Data Mappings for more details
+    dataMappings: () => {},
+    // Optional, see Calls To Action for more details
+    callsToAction: () => []
+  },
+  // Optional, configuration for what to display when no results are found.
+  noResults: {
+    // Optional, used to specify a custom template for the no results card, defaults to a built-in template.
+    template: '<div> <em>No results found!</em> Try again? </div>',
+    // Optional, whether to display all results in the vertical when no results are found. Defaults to false, in which case only the no results card will be shown.
+    displayAllResults: false
+  },
+
+  /**
+   * NOTE: The config options below are DEPRECATED.
+   * They will still work as expected, and the defaults will still be applied,
+   * but future major versions of the SDK will remove them.
+   * We recommend setting hideResultsHeader to true, and using the VerticalResultsCount and AppliedFilters components instead.
+   */
+  // Optional, whether to display the total number of results, default true
+  showResultCount: true,
+  // Optional, a custom template for the results count. You can specify the variables resultsCountStart, resultsCountEnd, and resultsCount.
+  resultsCountTemplate: '<div>{{resultsCountStart}} - {{resultsCountEnd}} of {{resultsCount}}</div>',
+  // Configuration for the applied filters bar in the header.
+  appliedFilters: {
+    // If true, show any applied filters that were applied to the vertical search. Defaults to true
+    show: true,
+    // If appliedFilters.show is true, whether to display the field name of an applied filter, e.g. "Location: Virginia" vs just "Virginia". Defaults to false.
+    showFieldNames: false,
+    // If appliedFilters.show is true, this is list of filters that should not be displayed.
+    // By default, builtin.entityType will be hidden
+    hiddenFields: ['builtin.entityType'],
+    // The character that separates the count of results (e.g. “1-6”) from the applied filter bar. Defaults to '|'
+    resultsCountSeparator: '|',
+    // If the filters are shown, whether or not they should be removable buttons. Defaults to false.
+    removable: false,
+    // The character that separates each field (and its associated filters) within the applied filter bar. Defaults to '|'
+    delimiter: '|',
+    // The aria-label given to the applied filters bar. Defaults to 'Filters applied to this search:'.
+    labelText: 'Filters applied to this search:',
+    // The aria-label given to the removable filter buttons.
+    removableLabelText: 'Remove this filter'
+  }
 })
 
-

FilterSearch Component

-

The FilterSearch component provides a text input box for users to type a query and select a preset matching filter. When a filter is selected, a vertical search is performed. If multiple FilterSearch components are on the page, the search will include all selected filters across all of the components.

-
<div class="filter-search-container"></div>
+

Vertical Results Count Component

+

The results count component displays the current results count on a vertical page.

+
ANSWERS.addComponent('VerticalResultsCount', {
+  container: '.results-count-container',
+  noResults: {
+    // Optional, whether the results count should be visible when displaying no results.
+    // Defaults to false.
+    visible: false
+  }
+});
 
-
ANSWERS.addComponent('FilterSearch', {
-  container: '.filter-search-container',
-  verticalKey: '<VERTICAL_KEY>',      // required
-  barKey: '<BAR_KEY>'                 // optional
+

Cards

+

Cards are used in Universal/Vertical Results for configuring the UI for a result on a per-item basis.

+

Cards take in a dataMappings attribute, which contains configuration for the card, and a callsToAction +attribute, which contains config for any callToAction buttons in the card.

+

callsToAction config is common throughout all cards, whereas different cards such as Standard vs BigImage +have specialized configuration depending on the card. See Calls To Action

+

There are three built-in cards, the Standard Card, the Accordion Card +and Legacy Card.

+

Calls To Action

+

callsToActions are specified as either an array of CTA configs, or a function that returns +an array of CTA configs. An array of CTA configs is an object of either static config options +or functions that return the desired config option.

+

Examples are detailed below.

+

Note: A CTA without both a label and icon will not be rendered.

+
    +
  1. an array of static CTA config objects
  2. +
+
const callsToAction = [{
+  // Label below the CTA icon, default null
+  label: 'cta label',
+  // Icon name for the CTA that is one of the built-in icons, defaults to undefined (no icon). If your icon
+  // is not recognized it will default to 'star'.
+  icon: 'star',
+  // URL to a custom icon for the cta. This takes priority over icon if both are present, default is
+  // no icon url.
+  iconUrl: 'https://urltomyicon.com/customicon.gif',
+  // Click through url for the icon and label
+  // Note, a protocol like https:// is required here.
+  url: 'https://yext.com',
+  // Analytics event that should fire, defaults to 'CTA_CLICK'. Other events outlined in the Analytics section.
+  analytics: 'CTA_CLICK',
+  // The target attribute for the CTA link, defaults to '_blank'. To open in a new window use '_blank'
+  target: '_blank',
+  // The eventOptions needed for the event to fire. Either a valid json string, an object, or a function that
+  // takes in the result data response.
+  // By default, if no event options are specified the SDK will try to add verticalKey, entityId, and searcher options
+  // to the analytics event.
+  eventOptions: result => {
+    return {
+      // The vertical key for the CTA. If unspecified, this defaults to the vertical key this cta is a part of
+      verticalKey: 'people',
+      // The entity id of the result this cta is a part of, defaults to the entityId field in Knowledge Graph
+      entityId: result.id,
+      // If the CTA is inside a vertical search, defaults to the value "VERTICAL",
+      // if is inside a universal search, defaults to the value "UNIVERSAL"
+      searcher: 'VERTICAL'
+    };
+  }
+}]
+
+
    +
  1. as a function that returns a cta config object. +NOTE: we do not allow multiple nested functions, to avoid messy user configurations.
  2. +
+
const callsToAction = item => [{
+  label: item._raw.name,
+  url: 'https://yext.com',
+  analyticsEventType: 'CTA_CLICK',
+  target: '_blank',
+  icon: 'briefcase',
+  eventOptions: `{ "verticalKey": "credit-cards", "entityId": "${item._raw.id}", "searcher":"UNIVERSAL", "ctaLabel": "cards"}`
+}, {
+  label: 'call now',
+  url: 'https://maps.google.com',
+  analyticsEventType: 'CTA_CLICK',
+  target: '_blank',
+  icon: 'phone',
+  eventOptions: `{ "verticalKey": "credit-cards", "entityId": "${item._raw.id}", "searcher": "UNIVERSAL", "ctaLabel": "cards"}`
+}]
+
+
    +
  1. Each individual field in a CTA config can also be a function that operates on the result item.
  2. +
+
const callsToAction = item => [{
+  label: item => item._raw.name,
+  url: 'https://yext.com',
+  analyticsEventType: 'CTA_CLICK',
+  target: '_self',
+  icon: 'briefcase',
+  eventOptions: `{ "verticalKey": "credit-cards", "entityId": "${item._raw.id}", "searcher": "UNIVERSAL", "ctaLabel": "cards"}`
+}]
+
+

callsToActions can then be included in a card object like so:

+
ANSWERS.addComponent('VerticalResults', {
+  /* ...other vertical results config... */
+  card: {
+    /* ...other card config...*/
+    callsToAction: item => [{
+      label: item => item._raw.name,
+      url: 'https://yext.com',
+    }]
+  }
+  /* ...other vertical results config... */
+})
+
+

Data Mappings

+

The dataMappings config option define how a card's attributes, such as title and details, will be rendered. +They can be configured either through a function that returns a dataMappings object +or a static dataMappings object.

+

Each attribute of a dataMappings object is also either a function or a static value.

+

Below is an example of dataMappings as function.

+
ANSWERS.addComponent('VerticalResults', {
+  /* ...other vertical results config... */
+  card: {
+    /* ...other card config...*/
+    dataMappings: item => ({
+      title: item.name,
+      subtitle: `Department: ${item.name} `,
+      details: item.description,
+      image: item.headshot ? item.headshot.url : '',
+      url: 'https://yext.com',
+      showMoreLimit: 500,
+      showMoreText: "show more",
+      showLessText: "put it back",
+      target: '_blank'
+    })
+  }
+  /* ...other vertical results config... */
 })
 
+

And below is an example of dataMappings as an object with functions inside it. +You can use both static attributes and function attributes together.

+
ANSWERS.addComponent('VerticalResults', {
+  /* ...other vertical results config... */
+  card: {
+    /* ...other card config...*/
+    dataMappings: {
+      title: item => item.name,
+      subtitle: item => `Department: ${item.name} `,
+      details: item => item.description,
+      image: item => item.headshot ? item.headshot.url : '',
+      url: 'https://yext.com',
+      showMoreLimit: 500,
+      showMoreText: 'show more',
+      showLessText: 'put it back',
+      target: '_blank'
+    }
+  }
+  /* ...other vertical results config... */
+})
+
+

Standard Card

+

The data mappings for a Standard Card has these attributes

+
const dataMappings = item => {
+  return {
+    // Title for the card, defaults to the name of the entity
+    title: item.title,
+    // Subtitle, defaults to null
+    subtitle: `Department: ${item.name} `,
+    // Details, defaults to the entity's description
+    details: item.description,
+    // Image to display, defaults to null
+    image: item.headshot ? item.headshot.url : '',
+    // Url for the title/subtitle, defaults to the entity's website url
+    // Note, a protocol like https://yext.com is required, as opposed to just yext.com
+    url: item.link || item.website,
+    // Character limit to hide remaining details and display a show more button, defaults to no limit.
+    showMoreLimit: 350,
+    // Text for show more button, defaults to 'Show More'
+    showMoreText: 'show more',
+    // Text for show less button, defaults to 'Show Less'
+    showLessText: 'put it back',
+    // The target attribute for the title link, defaults to '_self'. To open in a new window use '_blank'
+    target: '_blank',
+    // Whether to show the ordinal of this card in the results, i.e. first card is 1 second card is 2,
+    // defaults to false
+    showOrdinal: false,
+    // A tag to display on top of an image, always overlays the image, default no tag
+    tagLabel: 'On Sale!'
+  };
+}
+
+

Accordion Card

+

The data mappings for an Accordion Card has these attributes

+
const dataMappings = item => {
+  return {
+    // Title for the card, defaults to the name of the entity
+    title: item.title,
+    // Subtitle, defaults to null
+    subtitle: `Department: ${item.name} `,
+    // Details, defaults to the entity's description
+    details: item.description,
+    // Whether the first Accordion Card shown in vertical/universal results should be open on page load, defaults to false
+    expanded: false
+  };
+}
+
+

Legacy Card

+

The Legacy Card is very similar to the Standard Card, but with the legacy DOM structure and class names +from before v0.13.0. New users should not use the Legacy Card; instead, use the Standard Card. Features +added after v0.13.0 may not work with the Legacy Card.

+

The data mappings for a legacy card has these attributes

+
const dataMappings = item => {
+  return {
+    // Title for the card, defaults to the name of the entity
+    title: item.title,
+    // Subtitle, defaults to null
+    subtitle: `Department: ${item.name} `,
+    // Details, defaults to the entity's description
+    details: item.description,
+    // Image to display, defaults to null
+    image: item.headshot ? item.headshot.url : '',
+    // Url for the title/subtitle, defaults to the entity's website url
+    url: item.link || item.website,
+    // The target attribute for the title link, defaults to '_self'. To open in a new window use '_blank'
+    target: '_blank',
+    // Whether to show the ordinal of this card in the results, i.e. first card is 1 second card is 2,
+    // defaults to false
+    showOrdinal: false
+  };
+}
+
+

Pagination Component

+

This component is only for Vertical pages.

+

The Pagination component allows users to page through vertical search results.

+
<div class="pagination-container"></div>
+
+
ANSWERS.addComponent('Pagination', {
+  // Required, the selector for the container element where the component will be injected
+  container: '.pagination-component',
+  // Required*, the vertical for pagination, *if omitted, will fall back to the search base config
+  verticalKey: 'verticalKey',
+  // Optional, the maximum number of pages visible to non-mobile users. Defaults to 1.
+  maxVisiblePagesDesktop: 1,
+  // Optional, the maximum number of pages visible to mobile users. Defaults to 1.
+  maxVisiblePagesMobile: 1,
+  // Optional, ensure that the page numbers for first and last page are always shown. Not recommended to use with showFirstAndLastButton. Defaults to false.
+  pinFirstAndLastPage: false,
+  // Optional, display double-arrows allowing users to jump to the first and last page of results. Defaults to true.
+  showFirstAndLastButton: true,
+  // Optional, label for a page of results. Defaults to 'Page'.
+  pageLabel: 'Page',
+  // Optional, configuration for the pagination behavior when a query returns no results
+  noResults: {
+    // Optional, whether pagination should be visible when displaying no results.
+    // Defaults to false.
+    visible: false
+  },
+  // Function invoked when a user clicks to change pages. By default, scrolls the user to the top of the page.
+  onPaginate: (newPageNumber, oldPageNumber, totalPages) => {},
+  // DEPRECATED, please use showFirstAndLastButton instead.
+  // Display a double arrow allowing users to jump to the first page of results. Defaults to showFirstAndLastButton.
+  showFirst: true,
+  // DEPRECATED, please use showFirstAndLastButton instead.
+  // Display a double arrow allowing users to jump to the last page of results. Defaults to showFirstAndLastButton.
+  showLast: true,
+});
+

FilterBox Component

+

This component is only for Vertical pages.

The FilterBox component shows a list of filters to apply to a search.

<div class="filters-container"></div>
 
ANSWERS.addComponent('FilterBox', {
+  // Required, the selector for the container element where the component will be injected
   container: '.filters-container',
-  // List of filter component configurations
+  // Required, list of filter component configurations
   filters: [
     {
       type: 'FilterOptions',
@@ -355,12 +1171,12 @@ 

FilterBox Component

{ label: 'Open Now', field: 'c_openNow', - value: 'true' + value: true }, { label: 'Dog Friendly', field: 'c_dogFriendly', - value: 'true' + value: true }, { label: 'Megastores', @@ -369,140 +1185,1081 @@

FilterBox Component

} ] } - ] + ], + // Required, the vertical key for the search, default null + verticalKey: 'verticalKey', + // Optional, title to display above the filter + title: 'Filters', + // Optional, show number of results for each filter + showCount: true, + // Optional, execute a new search whenever a filter selection changes. If true, the Apply and Reset buttons will not display + searchOnChange: false, + // Optional, show a reset button per filter group, this will only display if searchOnChange is false + resetFilter: false, + // Optional, the label to use for the reset button above, this will only display if searchOnChange is false + resetFilterLabel: 'reset', + // Optional, show a reset-all button for the filter control. Defaults to displaying a reset button if searchOnChange is false. + resetFilters: true, + // Optional, the label to use for the reset-all button above, this will only display if resetFilters is true. + resetFiltersLabel: 'reset-all', + // Optional, allow collapsing excess filter options after a limit + showMore: true, + // Optional, the max number of filter to show before collapsing extras + showMoreLimit: 5, + // Optional, the label to show for displaying more filter + showMoreLabel: 'show more', + // Optional, the label to show for displaying less filter + showLessLabel: 'show less', + // Optional, allow expanding and collapsing entire groups of filters + expand: true, + // Optional, show the number of applied filter when a group is collapsed + showNumberApplied: true, + // Optional, the label to show on the apply button, this will only display if searchOnChange is false + applyLabel: 'apply', + // Optional, whether or not this filterbox contains dynamic filters, default false + isDynamic: true });
+

Facets Component

+

This component is only for Vertical pages.

+

The Facets component displays filters relevant to the current search, configured on the server, automatically. The Facets component will be hidden when a query returns no results. The selected options in a facets component will float to the top.

+
<div class="facets-container"></div>
+
+
ANSWERS.addComponent('Facets', {
+  // Required, the selector for the container element where the component will be injected
+  container: '.facets-container',
+  // Required
+  verticalKey: '<VERTICAL_KEY>',
+  // Optional, title to display above the facets
+  title: 'Filters',
+  // Optional, show number of results for each facet
+  showCount: true,
+  // Optional, execute a new search whenever a facet selection changes
+  searchOnChange: false,
+  // Optional, show a reset button per facet group
+  resetFacet: false,
+  // Optional, the label to use for the reset button above
+  resetFacetLabel: 'reset',
+  // Optional, show a reset-all button for the facets control. Defaults to showing a reset-all button if searchOnChange is false.
+  resetFacets: true,
+  // Optional, the label to use for the reset-all button above
+  resetFacetsLabel: 'reset-all',
+  // Optional, allow collapsing excess facet options after a limit
+  showMore: true,
+  // Optional, the max number of facets to show before collapsing extras
+  showMoreLimit: 5,
+  // Optional, the label to show for displaying more facets
+  showMoreLabel: 'show more',
+  // Optional, the label to show for displaying less facets
+  showLessLabel: 'show less',
+  // Optional, allow expanding and collapsing entire groups of facets
+  expand: true,
+  // Optional, show the number of applied facets when a group is collapsed
+  showNumberApplied: true,
+  // Optional, the placeholder text used for the filter option search input
+  placeholderText: 'Search here...',
+  // Optional, if true, display the filter option search input
+  searchable: false,
+  // Optional, the form label text for the search input, defaults to 'Search for a filter option'
+  searchLabelText: 'Search for a filter option',
+  // Optional, a transform function which is applied to an array of facets
+  // See the "Transforming Facets" section below for more info
+  transformFacets: (facets, config => facets),
+  // DEPRECATED, please use transformFacets instead. This option is disabled if transformFacets is supplied
+  // Optional, field-specific overrides for a filter
+  fields: {
+    'c_customFieldName':  { // Field id to override e.g. c_customFieldName, builtin.location
+      // Optional, the placeholder text used for the filter option search input
+      placeholderText: 'Search here...',
+      // Optional, show a reset button per facet group
+      showReset: false,
+      // Optional, the label to use for the reset button above
+      resetLabel: 'reset',
+      // Optional, if true, display the filter option search input
+      searchable: false,
+      // Optional, the form label text for the search input, defaults to 'Search for a filter option'
+      searchLabelText: 'Search for a filter option',
+      // Optional, control type, singleoption or multioption
+      control: 'singleoption',
+      // Optional, override the field name for this facet
+      label: 'My custom field'
+      // Optional, allow collapsing excess facet options after a limit
+      showMore: true,
+      // Optional, the max number of facets to show before collapsing extras
+      showMoreLimit: 5,
+      // Optional, the label to show for displaying more facets
+      showMoreLabel: 'show more',
+      // Optional, the label to show for displaying less facets
+      showLessLabel: 'show less',
+      // Optional, allow expanding and collapsing entire groups of facets
+      expand: true,
+      // Optional, callback function for when a facet is changed
+      onChange: function() { console.log('Facet changed'); },
+      // Optional, the selector used for options in the template, defaults to '.js-yext-filter-option'
+      optionSelector: '.js-yext-filter-option',
+    }
+  },
+  // Optional, the label to show on the apply button
+  applyLabel: 'apply'
+});
+
+

Transforming Facets

+

The transformFacets option of the Facets component allows facets data to be fully customized. The function takes in and returns an array of the answers-core DisplayableFacet which is described here. The function also has access to the Facets config as the second parameter.

+

Here's an example of using this option to customize a boolean facet.

+
transformFacets: facets => {
+  return facets.map(facet => {
+    const options = facet.options.map(option => {
+      let displayName = option.displayName;
+      if (facet.fieldId === 'c_acceptingNewPatients') {
+        if (option.value === false) { displayName = "Not Accepting Patients"; }
+        if (option.value === true) { displayName = "Accepting Patients"; }
+      }
+      return Object.assign({}, option, { displayName });
+    });
+    return Object.assign({}, facet, { options });
+  });
+},
+
+

FilterSearch Component

+

The FilterSearch component provides a text input box for users to type a query and select a preset matching filter. When a filter is selected, a vertical search is performed, and the filter and query are stored in the url. If multiple FilterSearch components are on the page, the search will include all selected filters across all of the components.

+
<div class="filter-search-container"></div>
+
+
ANSWERS.addComponent('FilterSearch', {
+  // Required, the selector for the container element where the component will be injected
+  container: '.filter-search-container',
+  // Required
+  verticalKey: '<VERTICAL_KEY>',
+  // Optional, no default
+  placeholderText: 'Start typing...',
+  // Optional, if true, the selected filter is saved and used for the next search,
+  // but does not trigger a search itself. Defaults to false.
+  storeOnChange: true,
+  // Optional, defaults to native form node within container
+  formSelector: '.js-form',
+  // Optional, the input element used for searching and wires up the keyboard interaction
+  inputEl: '.js-query',
+  // Optional, provided to the template as a data point
+  title: 'title',
+  // Optional, the search text used for labeling the input box, also provided to template
+  searchText: 'What do you want to search',
+  // Optional, the query text to show as the first item for auto complete
+  promptHeader: 'Header',
+  // Optional, auto focuses the input box if set to true, default false
+  autoFocus: true,
+  // Optional, redirect search query to url
+  redirectUrl: 'path/to/url',
+  // Optional, the query displayed on load. Defaults to the query stored in the url (if any).
+  query: 'Green Ice Cream Flavor',
+  // Optional, the filter for filtersearch to apply on load, defaults to the filter stored in the url (if any).
+  // An example filter is shown below. For more information see the filter section of
+  // https://developer.yext.com/docs/api-reference/#operation/KnowledgeApiServer.listEntities
+  filter: {
+    c_iceCreamFlavors: {
+      $eq: 'pistachio'
+    }
+  },
+  // Optional, the search parameters for autocompletion
+  searchParameters: {
+    // List of fields to query for
+    fields: [{
+      // Field id to query for e.g. c_customFieldName, builtin.location
+      fieldId: 'builtin.location',
+      // Entity type api name e.g. healthcareProfessional, location, ce_person
+      entityTypeId: 'ce_person',
+    }]
+    // Optional, if true sections search results by search filter, default false
+    sectioned: false,
+  }
+})
+

Filter Components

Filter components can be used in a FilterBox or on their own to affect a search.

FilterOptions

-

FilterOptions displays a set of filters with either checkboxes or radio buttons.

+

FilterOptions displays a set of filters with either checkboxes or radio buttons. +As a user interacts with FilterOptions, information on which options are selected +is stored in the url. Returning to that same url will load the page with those saved +options already selected.

<div class="filter-container"></div>
 
ANSWERS.addComponent('FilterOptions', {
+  // Required, the selector for the container element where the component will be injected
   container: '.filter-container',
-  // Control type, singleoption or multioption
+  // Required, control type: 'singleoption' or 'multioption'
   control: 'singleoption',
-  // List of options
+  // The type of options to filter by, either 'STATIC_FILTER' or 'RADIUS_FILTER'.
+  // Defaults to 'STATIC_FILTER'.
+  optionType: 'STATIC_FILTER',
+  // Required, list of options
+  options: [
+    /** Depends on the above optionType, either 'STATIC_FILTER' or 'RADIUS_FILTER', see below. **/
+  ],
+  // Optional, if true, the filter value is saved on change and sent with the next search. Defaults to true.
+  storeOnChange: true,
+  // Optional, the selector used for options in the template, defaults to '.js-yext-filter-option'
+  optionSelector: '.js-yext-filter-option',
+  // Optional, if true, show a reset button
+  showReset: false,
+  // Optional, the label to use for the reset button, defaults to 'reset'
+  resetLabel: 'reset',
+  // Optional, allow collapsing excess filter options after a limit, defaults to true
+  showMore: true,
+  // Optional, the max number of filter options to show before collapsing extras, defaults to 5
+  showMoreLimit: 5,
+  // Optional, the label to show for displaying more options, defaults to 'show more'
+  showMoreLabel: 'show more',
+  // Optional, the label to show for displaying less options, defaults to 'show less'
+  showLessLabel: 'show less',
+  // Optional, allow expanding and collapsing the filter, defaults to true
+  showExpand: true,
+  // Optional, show the number of applied options when a group is collapsed, defaults to true
+  showNumberApplied: true,
+  // Optional, the callback function to call when changed
+  onChange: function() {},
+  // Optional, the label to be used in the legend, defaults to 'Filters'
+  label: 'Filters',
+  // Optional, the placeholder text used for the filter option search input
+  placeholderText: 'Search here...',
+  // Optional, if true, display the filter option search input
+  searchable: false,
+  // Optional, the form label text for the search input, defaults to 'Search for a filter option'
+  searchLabelText: 'Search for a filter option',
+});
+
+

The options config varies depending on whether the optionType is 'STATIC_FILTER' or 'RADIUS_FILTER'. +A STATIC_FILTER allows you to filter on a specified field, while a RADIUS_FILTER allows you to filter +results based on their distance from the user.

+
STATIC_FILTER
+
{
   options: [
     {
-      // Label to show next to the filter option
-      label: 'Open Now',
-      // The api field to filter on, configured on the Yext platform
+      // Required, the api field to filter on, configured on the Yext platform.
       field: 'c_openNow',
-      // The value for the above field to filter by
-      value: 'true'
+      // Required, the value for the above field to filter by.
+      value: true,
+      // Optional, the label to show next to the filter option.
+      label: 'Open Now',
+      // Optional, whether this option will be selected on page load. Selected options stored in the url
+      // take priority over this. Defaults to false.
+      selected: false
     },
     {
-      label: 'Dog Friendly',
       field: 'c_dogFriendly',
-      value: 'true'
+      value: true,
+      label: 'Dog Friendly',
+      selected: true
     },
     {
-      label: 'Megastores',
       field: 'c_storeType',
-      value: 'Megastore'
+      value: 'Megastore',
+      label: 'Megastores'
     }
   ]
+}
+
+
RADIUS_FILTER
+
{
+  options: [
+    {
+      // Required, the value of the radius to apply (in meters). If this value is 0, the SDK will not add explicit radius filtering to the request. The backend may still perform its own filtering depending on the query given.
+      value: 8046.72,
+      // Optional, the label to show next to the filter option.
+      label: '5 miles',
+      // Optional, whether this option will be selected on page load. Selected options stored in the url
+      // take priority over this. Defaults to false.
+      selected: false
+    },
+    {
+      value: 16093.4,
+      label: '10 miles',
+      selected: true
+    },
+    {
+      value: 40233.6,
+      label: '25 miles'
+    },
+    {
+      value: 80467.2,
+      label: '50 miles'
+    },
+    {
+      value: 0,
+      label: "Do not filter by radius"
+    }
+  ],
+}
+
+

RangeFilter

+

Displays two numeric inputs for selecting a number range.

+
<div class="range-filter-container"></div>
+
+
ANSWERS.addComponent('RangeFilter', {
+  // Required, the selector for the container element where the component will be injected
+  container: '.range-filter-container',
+  // Required, the API name of the field to filter on
+  field: 'outdoorPoolCount',
+  // Optional, title to display for the range control, defaults to empty legend
+  title: 'Number of Outdoor Pools',
+  // Optional, the label to show next to the min value, defaults to no label
+  minLabel: 'At Least',
+  // Optional, the placeholder text for the min value, defaults to 'Min'
+  minPlaceholderText: 'Min',
+  // Optional, the label to show next to the max value, defaults to no label
+  maxLabel: 'Not More Than',
+  // Optional, the placeholder text for the max value, defaults to 'Max'
+  maxPlaceholderText: 'Max',
+  // Optional, the initial min value to show, defaults to 0. Set this to null to clear the value.
+  initialMin: 1,
+  // Optional, the initial max value to show, defaults to 10. Set this to null to clear the value.
+  initialMax: 5,
+  // Optional, the callback function to call when changed
+  onChange: function() {}
 });
 
-

Direct Answer Component

-

The Direct Answer Component will render the BEST result, if found, -based on the query.

-
<div class="direct-answer-container"></div>
+

DateRangeFilter

+

Displays two date inputs for selecting a range of dates.

+
<div class="date-range-filter-container"></div>
 
-
ANSWERS.addComponent('DirectAnswer', {
-  container: '.direct-answer-container'
-})
+
ANSWERS.addComponent('DateRangeFilter', {
+  // Required, the selector for the container element where the component will be injected
+  container: '.date-range-filter-container',
+  // Required, the API name of the field to filter on
+  field: 'time.start',
+  // Optional, title to display for the range, defaults to empty legend
+  title: 'Event Start Date',
+  // Optional, the label to show next to the min date, defaults to no label
+  minLabel: 'Earliest',
+  // Optional, the label to show next to the max date, defaults to no label
+  maxLabel: 'Latest',
+  // Optional, the initial min date to show in yyyy-mm-dd format, defaults to today. Set this to null to clear the value.
+  initialMin: '2019-08-01',
+  // Optional, the initial max date to show in yyyy-mm-dd format, defaults to today. Set this to null to clear the value.
+  initialMax: '2019-09-01',
+  // Optional, whether to store the filter on change to input
+  storeOnChange: true,
+  // Optional, if true, this filter represents an exclusive range, rather than an inclusive one, defaults to false
+  isExclusive: false,
+  // Optional, the callback function to call when changed
+  onChange: function() {}
+});
 
-

Universal Results Component

-

The Universal Results component will render the results of a query, -across all configured verticals, seperated by sections.

-

The most complex component has a ton of overridable configuration options.

-
<div class="universal-results-container"></div>
+

GeoLocationFilter

+

Displays a "Use My Location" button that filters results to a radius around the user's current position.

+
<div class="geolocation-filter-container"></div>
 
-

Basic Component

-
ANSWERS.addComponent('UniversalResults', {
-  container: '.universal-results-container',
+

For all optional config in the example, unless otherwise specified, the default is the example value.

+
ANSWERS.addComponent('GeoLocationFilter', {
+  // Required, the selector for the container element where the component will be injected
+  container: '.geolocation-filter-container',
+  // Optional, the vertical key to use
+  verticalKey: 'verticalKey',
+  // Optional, radius around the user, in miles, to find results, default 50
+  radius: 50,
+  // Optional, the text to show when enabled
+  enabledText: 'Disable My Location',
+  // Optional, the text to show ehn loading the user's location
+  loadingText: 'Loading',
+  // Optional, The label to show when unable to get the user's location
+  errorText: 'Unable To Use Location',
+  // Optional, CSS selector of the button
+  buttonSelector: '.js-yxt-GeoLocationFilter-button',
+  // Optional, Css selector of the query input
+  inputSelector: '.js-yxt-GeoLocationFilter-input',
+  // Optional, if true, triggers a search on each change to a filter, default false
+  searchOnChange: true,
+  // Optional, the icon url to show in the geo button
+  geoButtonIcon: 'path/to/url',
+  // Optional, the alt text to use with the geo button's icon
+  geoButtonIconAltText: 'Use My Location',
+  // Optional, the text to show in the geo button
+  geoButtonText: 'Use my location',
+  // Optional, Search parameters for the geolocation autocomplete
+  searchParameters: {
+    // List of fields to query for
+    fields: [{
+      // Field id to query for e.g. c_customFieldName, builtin.location
+      fieldId: 'builtin.location',
+      // Entity type api name e.g. healthcareProfessional, location, ce_person
+      entityTypeId: 'ce_person',
+      // Optional, if true sections search results by search filter, default false
+      sectioned: false,
+    }]
+  },
+  // Optional, options to pass to the geolocation api, which is used to fetch the user's current location.
+  // https://developer.mozilla.org/en-US/docs/Web/API/PositionOptions
+  geolocationOptions: {
+    // Optional, whether to improve accuracy at the cost of response time and/or power consumption, defaults to false.
+    enableHighAccuracy: false,
+    // Optional, the maximum amount of time (in ms) a geolocation call is allowed to take before defaulting, defaults to 6 seconds.
+    timeout: 6000,
+    // Optional, the maximum amount of time (in ms) to cache a geolocation call, defaults to 5 minutes.
+    maximumAge: 300000,
+  },
+  // Optional, options for an alert when the geolocation call fails.
+  geolocationTimeoutAlert: {
+    // Optional, whether to display a window.alert() on the page, defaults to false.
+    enabled: false,
+    // Optional, the message in the alert. Defaults to the below
+    message: "We are unable to determine your location"
+  }
+});
+
+

Applied Filters Component

+

The Applied Filters Component displays your currently applied filters as a row of text tags, labeled +by filter display value. If the "removable" config option is set to true, these text tags will instead +be "removable filters", which, when clicked, will remove the clicked filter from the search. +Only intended for vertical pages.

+
ANSWERS.addComponent('AppliedFilters', {
+  container: '.applied-filters-container',
+  // Optional, The vertical key of your search. Defaults to the vertical key specified in the search config.
+  verticalKey: 'aVerticalKey',
+  // Optional, Whether to display the field name of each group of applied filters. e.g. "Location: Virginia, New York" vs just "Virginia, New York". Defaults to false.
+  showFieldNames: false,
+  // Optional, This is list of filters that should not be displayed. Defaults to hiding ['builtin.entityType'].
+  hiddenFields: ['builtin.entityType'],
+  // Optional, Whether or not the displayed filters should be removable filters, or just simple text tags. Defaults to false (text tags).
+  removable: false,
+  // Optional, The character that separates each group of filters (grouped by field name). Defaults to '|'.
+  delimiter: '|',
+  // Optional, The aria-label given to the component. Defaults to 'Filters applied to this search:'.
+  labelText: 'Filters applied to this search:',
+  // Optional, The aria-label given to the removable filters. Defaults to 'Remove this filter'.
+  removableLabelText: 'Remove this filter'
+});
+
+

Navigation Component

+

The Navigation Component adds a dynamic experience to your pages navigation experience.

+

When using multiple vertical searches in a universal search, the navigation ordering will be automatically updated based on the search results. By default, tabs that do not fit in the container will go inside a dropdown menu.

+

Vertical configurations should be provided the ANSWERS.init's verticalPages configuration. Find more info in the Vertical Pages Configuration section.

+
<div class="navigation-container"></nav>
+
+
ANSWERS.addComponent('Navigation', {
+  // Required, the selector for the container element where the component will be injected
+  container: '.navigation-container',
+  // Optional, controls if navigation shows a scroll bar or dropdown for mobile. Options are COLLAPSE and INNERSCROLL
+  mobileOverflowBehavior: 'COLLAPSE',
+  // Optional, the aria-label to set on the navigation, defaults to 'Search Page Navigation'
+  ariaLabel: 'Search Page Navigation',
+  // Optional, the label to display on the dropdown menu button when it overflows, defaults to 'More'
+  overflowLabel: 'More',
+  // Optional, name of the icon to show on the dropdown button instead when it overflows
+  overflowIcon: null,
 })
 
-

Custom Render for ALL Result Items

-

You can override the render function for EACH item in the result list, -as apposed to the entire component.

-
ANSWERS.addComponent('UniversalResults', {
-  container: '.universal-results-container',
-  renderItem: function(data) {
-    return `my item ${data.name}`
-  }
+

QA Submission Component

+

The QA Submission component provides a form for submitting a QA question, +when a search query is run.

+
<div class="question-submission-container"></div>
+
+
ANSWERS.addComponent('QASubmission', {
+  // Required, the selector for the container element where the component will be injected
+  container: '.question-submission-container',
+  // Required. Set this to the Entity ID of the organization entity in the Knowledge Graph
+  entityId: 123,
+  // Required. Defaults to ''
+  privacyPolicyUrl: 'https://mybiz.com/policy',
+  // Optional, defaults to native form node within container
+  formSelector: '.js-form',
+  // Optional, ;abel for name input
+  nameLabel: 'Name',
+  // Optional, label for email input
+  emailLabel: 'Email',
+  // Optional, label for question input
+  questionLabel: 'Question',
+  // Optional, title displayed for the form
+  sectionTitle: 'Ask a question',
+  // Optional, teaser displayed for the form, next to the title
+  teaser: 'Can\'t find what you’re looking for? Ask a question below.',
+  // Optional, description for the form
+  description: 'Enter your question and contact information, and we\'ll get back to you with a response shortly.'
+  // Optional, text before the privacy policy link
+  privacyPolicyText: 'By submitting my email address, I consent to being contacted via email at the address provided.',
+  // Optional, label for the privacy policy url
+  privacyPolicyUrlLabel: 'Learn more here.',
+  // Optional, error message displayed when the privacy policy is not selected
+  privacyPolicyErrorText: '* You must agree to the privacy policy to submit feedback.',
+  // Optional, error message displayed when an invalid email is not submitted
+  emailFormatErrorText: '* Please enter a valid email address.'
+  // Optional, placeholder displayed in all required fields
+  requiredInputPlaceholder: '(required)',
+  // Optional, confirmation displayed once a question is submitted
+  questionSubmissionConfirmationText: 'Thank you for your question!',
+  // Optional, label displayed on the button to submit a question
+  buttonLabel: 'Submit',
+  // Optional, set this to whether or not the form is expanded by default when a user arrives on the  page
+  expanded: true,
+  // Optional, error message displayed when there is an issue with the QA Submission request
+  networkErrorText: 'We\'re sorry, an error occurred.'
 })
 
-

Custom Template for ALL Result Items

-

You can override the handlebars template for EACH item in the result list, -as apposed to the entire component.

-
ANSWERS.addComponent('UniversalResults', {
-  container: '.universal-results-container',
-  itemTemplate: `my item {{name}}`
+

Spell Check Component

+

The spell check component shows spell check suggestions/autocorrect.

+
<div class="spell-check-container"></div>
+
+
ANSWERS.addComponent('SpellCheck', {
+  // Required, the selector for the container element where the component will be injected
+  container: '.spell-check-container',
+  // Optional, the help text to display when suggesting a query
+  suggestionHelpText: 'Did you mean:',
 })
 
-

Custom Render For Specific Vertical Result Items

-

You can override the render function for a particular section within the results list, -by providing a vertical search config id as the context, and using the same options as above.

-
ANSWERS.addComponent('UniversalResults', {
-  container: '.universal-results-container',
-  config: {
-    'locations': { // The vertical search config id
-      renderItem: function(data) {
-        return `my item ${data.name}`;
-      }
-    }
+

Location Bias Component

+

The location bias component shows location that used for location bias and allow user to improve accuracy with HTML5 geolocation.

+
<div class="location-bias-container"></div>
+
+
ANSWERS.addComponent('LocationBias', {
+  // Required, the selector for the container element where the component will be injected
+  container: '.location-bias-container',
+  // Optional, the vertical key for the search, default null
+  verticalKey: 'verticalKey',
+  // Optional, the element used for updating location
+  updateLocationEl: '.js-locationBias-update-location',
+  // Optional, help text to inform someone their IP was used for location
+  ipAccuracyHelpText: 'based on your internet address',
+  // Optional, help text to inform someone their device was used for location
+  deviceAccuracyHelpText: 'based on your device',
+  // Optional, text used for the button to update location
+  updateLocationButtonText: 'Update your location',
+  // Optional, options to pass to the geolocation api, which is used to fetch the user's current location.
+  // https://developer.mozilla.org/en-US/docs/Web/API/PositionOptions
+  geolocationOptions: {
+    // Optional, whether to improve accuracy at the cost of response time and/or power consumption, defaults to false.
+    enableHighAccuracy: false,
+    // Optional, the maximum amount of time (in ms) a geolocation call is allowed to take before defaulting, defaults to 6 seconds.
+    timeout: 6000,
+    // Optional, the maximum amount of time (in ms) to cache a geolocation call, defaults to 5 minutes.
+    maximumAge: 300000,
+  },
+  // Optional, options for an alert when the geolocation call fails.
+  geolocationTimeoutAlert: {
+    // Optional, whether to display a window.alert() on the page, defaults to false.
+    enabled: false,
+    // Optional, the message in the alert. Defaults to the below
+    message: "We are unable to determine your location"
   }
 })
 
-

Custom Template For Specific Vertical Result Items

-

You can override the handlebars template for a particular section within the results list, -by providing a vertical search config id as the context, and using the same options as above.

-
ANSWERS.addComponent('UniversalResults', {
-  container: '.universal-results-container',
-  config: {
-    'locations': { // The vertical search config id
-      itemTemplate: `my item {{name}}`
+

Sort Options Component

+

The sort options component displays a list of radio buttons that allows users to sort the results of a vertical search. When a query returns no results, the component will not be rendered on the page. +Currently, there may be only one sort options component per page.

+
<div class='sort-options-container'></div>
+
+
// note: showExpand and showNumberApplied options are explicitly not included:
+// sorting will always be exposed to the user if added.
+ANSWERS.addComponent('SortOptions', {
+  // Required, the selector for the container element where the component will be injected
+  container: '.sort-options-container',
+  // Optional: The label used for the “default” sort (aka sort the order provided by the config), defaults to 'Best Match'
+  defaultSortLabel: 'Best Match',
+  // Required: List of component configurations
+  options: [
+    {
+      // Required: Either FIELD, ENTITY_DISTANCE, or RELEVANCE
+      type: 'FIELD',
+      // Required only if type is FIELD, field name to sort by
+      field: 'c_popularity',
+      // Direction to sort by, either 'ASC' or 'DESC'
+      // Required only if type is FIELD
+      direction: 'ASC',
+      // Required: Label for the sort option's radio button
+      label: 'Popularity',
+    },
+    {
+      type: "ENTITY_DISTANCE",
+      label: 'Distance'
+    },
+    {
+      type: 'RELEVANCE',
+      label: 'Relevance'
     }
+  ],
+  // Required: the vertical key used
+  verticalKey: 'KM',
+  // Optional: the selector used for options in the template, defaults to '.yxt-SortOptions-optionSelector'
+  optionSelector: '.yxt-SortOptions-optionSelector',
+  // Optional: if true, triggers a resorts on each change to the sort options,
+  // if false the component also renders an apply button that applies the sort, defaults to false
+  searchOnChange: false,
+  // Optional: Show a reset button, defaults to false
+  showReset: false,
+  // Optional: The label to use for the reset button, defaults to 'reset'
+  resetLabel: 'reset',
+  // Optional: Allow collapsing excess filter options after a limit, defaults to true
+  // Note: screen readers will not read options hidden by this flag, without clicking show more first
+  showMore: true,
+  // Optional: The max number of filter options to show before collapsing extras, defaults to 5
+  showMoreLimit: 5,
+  // Optional: The label to show for displaying more options, defaults to 'Show more'
+  showMoreLabel: 'Show more',
+  // Optional: The label to show for displaying less options, defaults to 'Show less'
+  showLessLabel: 'Show less',
+  // Optional, the callback function to call when changed, defaults to function() => {}
+  // runs BEFORE search triggered by searchOnChange if searchOnChange is true
+  onChange: function() {},
+  // Optional, the label to be used in the legend, defaults to 'Sorting'
+  label: 'Sorting',
+  // Optional, the label to be used on the apply button
+  // only appears if searchOnChange is false, defaults to 'Apply'
+  applyLabel: 'Apply'
+});
+
+

Map Component

+

The Map component displays a map with a pin for each result that has Yext display coordinates.

+
<div class='map-container'></div>
+
+
ANSWERS.addComponent('Map', {
+  // Required. This is the class of the target HTML element the component will be mounted to
+  container: '.map-container',
+  // Required. Supported map providers include: `google` or `mapBox`, not case-sensitive
+  mapProvider: 'mapBox',
+  // Required*. The API Key used for interacting with the map provider; (*except for Google Maps if provided `clientId`)
+  apiKey: '',
+  // Optional, can be used for Google Maps in place of the API key
+  clientId: '',
+  // Optional, used to determine the language of the map. Defaults to the locale specified in the ANSWERS init.
+  // Refer to the section "Supported Map Locales" below for the list of supported locales.
+  locale: 'en',
+  // Optional, determines whether or not to collapse pins at the same lat/lng
+  collapsePins: false,
+  // Optional, the zoom level of the map, defaults to 14
+  zoom: 14,
+  // Optional, the default coordinates to display if there are no results returned used if showEmptyMap is set to true
+  defaultPosition: { lat: 37.0902, lng: -95.7129 },
+  // Optional, determines if an empty map should be shown when there are no results. Defaults to false.
+  showEmptyMap: false,
+  // Optional, callback to invoke when a pin is clicked. The clicked item(s) are passed to the callback
+  onPinClick: null,
+  // Optional, callback to invoke when a pin is hovered. The clicked item(s) are passed to the callback
+  onPinMouseOver: null,
+  // Optional, callback to invoke when a pin is no longer hovered after being hovered. The clicked item(s) are passed to the callback
+  onPinMouseOut: null,
+  // Optional, callback to invoke once the Javascript is loaded
+  onLoaded: function () {},
+  // Optional, configuration for the map's behavior when a query returns no results
+  noResults: {
+    // Optional, whether to display map pins for all possible results when no results are found. Defaults to false.
+    displayAllResults: false,
+    // Optional, whether to display the map when no results are found, taking priority over showEmptyMap. If unset, a map will be visible if showEmptyMap is true OR if displayAllResults is true and alternative results are returned.
+    visible: false
+  },
+  // Optional, the custom configuration override to use for the map markers, function
+  pin: function () {
+    return {
+      icon: {
+        anchor: null, // e.g. { x: 1, y: 1 }
+        svg: null,
+        url: null,
+        scaledSize: null // e.g. { w: 20, h: 20 }
+      },
+      labelType: 'numeric'
+    };
+  },
+};
+
+

Supported Map Locales

+

When using MapBox (mapBox) as the mapProvider, the valid locale options +are the ones listed here:

+
    +
  • Arabic: ar
  • +
  • Chinese: zh
  • +
  • English: en
  • +
  • French: fr
  • +
  • German: de
  • +
  • Japanese: ja
  • +
  • Korean: ko
  • +
  • Portuguese: pt
  • +
  • Russian: ru
  • +
  • Spanish: es
  • +
+

When using Google Maps (google) as the mapProvider, the valid locale options can be found here.

+

Icon Component

+

The Icon Component will typically be created by other components, but it can be used as a standalone component as well.

+
<div class='icon-container'></div>
+
+
ANSWERS.addComponent('IconComponent', {
+  // Required. This is the class of the target HTML element the component will be mounted to.
+  container: '.icon-container',
+  // Optional. Can be used to access an icon defined within the Answers system. See below for default icon names.
+  iconName: 'default',
+  // Optional. Sets the icon to reference an image URL. Overrides icon name.
+  iconUrl: '',
+  // Optional. Adds class names to the icon. Multiple classnames should be space-delimited.
+  classNames: '',
+});
+
+

The following is a list of names for the icons that are supported by default.

+
    +
  • briefcase
  • +
  • calendar
  • +
  • callout
  • +
  • chevron
  • +
  • close
  • +
  • directions
  • +
  • document
  • +
  • elements
  • +
  • email
  • +
  • gear
  • +
  • info
  • +
  • kabob
  • +
  • light_bulb
  • +
  • link
  • +
  • magnifying_glass
  • +
  • mic
  • +
  • office
  • +
  • pantheon
  • +
  • person
  • +
  • phone
  • +
  • pin
  • +
  • receipt
  • +
  • star
  • +
  • support
  • +
  • tag
  • +
  • thumb
  • +
  • window
  • +
  • yext_animated_forward
  • +
  • yext_animated_reverse
  • +
  • yext
  • +
+

Customizing Components

+

Using a Custom Renderer

+

If you want to use a use your own template language (e.g. soy, mustache, groovy, etc), +you should NOT use the template argument. Instead, you can provide a custom render function to the component.

+
ANSWERS.addComponent('SearchBar', {
+  container: '.search-container',
+  render: function(data) {
+    // Using native ES6 templates -- but you can replace this with soy,
+    // or any other templating language as long as it returns a string.
+    return `<div class="my-search">${data.title}</div>`
   }
 })
 
-

Vertical Results Component

-

The Vertical Results component shares all the same configurations from Universal Results, but you don't need to specifiy a config or context.

-

You define all the options at the top level object.

-
<div class="results-container"></div>
+

Custom Data Formatting

+

You can format specific entity fields using fieldFormatters. +These formatters are applied before the transformData step.

+

Each formatter takes in an object with the following properties :

+
    +
  • entityProfileData
  • +
  • entityFieldValue
  • +
  • highlightedEntityFieldValue
  • +
  • verticalId
  • +
  • isDirectAnswer
  • +
+

Below is an example usage.

+
ANSWERS.init({
+  apiKey: '<API_KEY_HERE>',
+  experienceKey: '<EXPERIENCE_KEY_HERE>',
+  fieldFormatters: {
+    'name': (formatterObject) => formatterObject.entityFieldValue.toUpperCase(),
+    'description' : (formatterObject) => formatterObject.highlightedEntityFieldValue
+  }
+});
 
-
ANSWERS.addComponent('VerticalResults', {
-  container: '.results-container',
+

Custom Data Transforms

+

If you want to mutate the data thats provided to the render/template before it gets rendered, +you can use the transformData hook.

+

All properties and values that you return from here will be accessible from templates.

+
ANSWERS.addComponent('SearchBar', {
+  container: '.search-container',
+  transformData: (data) => {
+    // Extend/overide the data object
+    return Object.assign({}, data, {
+      title: data.title.toLowerCase()
+    })
+  },
+  render: function(data) {
+    // Using native ES6 templates -- but you can replace this with soy,
+    // or any other templating language as long as it returns a string.
+    return `<div class="my-search">${data.title}</div>`
+  }
 })
 
-

QA Submission Component

-

The QA Submission component provides a form for submitting a QA question, -when a search query is run.

-
<div class="question-submission-container"></div>
+

Using a Custom Template for a Component

+

All component templates are written using Handlebars templates.

+

It's easy to override these templates with your own templates. +Keep in mind, that you must provide valid handlebars syntax here.

+
// Use handlebars syntax to create a template string
+let customTemplate = `<div class="my-search">{{title}}</div>`
+
+ANSWERS.addComponent('SearchBar', {
+  container: '.search-container',
+  template: customTemplate
+})
 
-
ANSWERS.addComponent('QASubmission', {
-  container: '.question-submission-container',
-  nameLabel: 'Your Name:',             // Optional, defaults to 'Name:'
-  emailLabel: '*Email:',               // Optional, defaults to '*Email:'
-  questionLabel: 'Ask us anything!:',  // Optional, defaults to 'What is your question?'
-  privacyPolicyLabel: 'I agree!',      // Optional, defaults to 'I agree to our policy:',
-  buttonLabel: 'Submit'                // Optional,  defaults to 'Submit:'
+

The SDK also offers an ANSWERS.registerTemplate function. This will map +a template string to an entry in the Answers handlebars renderer.

+
  /**
+   * Compile and add a template
+   * @param {string} templateName The unique name for the template
+   * @param {string} template The handlebars template string
+   */
+  registerTemplate (templateName, template)
+
+

The default handlebars renderer uses a mapping from template name strings to +handlebars template strings. If, while trying to register a template, the +template name does not exist, a new template entry is created. If the name +already exists, the current template for the template name is overriden. This +allows you override default template names.

+

For example:

+
  // override current SpellCheck template
+  ANSWERS.registerTemplate(
+    'search/spellcheck',
+    '<div class="Mine">Did you mean {{correctedQuery}}?</div>'
+  );
+
+  // create new Card template
+  ANSWERS.registerTemplate(
+    'cards/custom',
+    '<p>Card content</p>'
+  );
+
+

Creating Custom Components

+

You can create custom Answers components with the same power of the builtin components. First, create +a subtype of ANSWERS.Component and register it.

+

For ES6:

+
class MyCustomComponent extends ANSWERS.Component {
+  constructor (config) {
+    super(config);
+    this.myProperty = config.myProperty;
+  }
+
+  static defaultTemplateName () {
+    return 'default';
+  }
+
+  static areDuplicateNamesAllowed () {
+    return false;
+  }
+
+  static get type () {
+    return 'MyCustomComponent';
+  }
+}
+ANSWERS.registerComponentType(MyCustomComponent); // Register the component with the library
+
+

For ES5:

+
function MyCustomComponent (config) {
+  ANSWERS.Component.call(this, config);
+
+  this.myProperty = config.myProperty;
+}
+
+MyCustomComponent.prototype = Object.create(ANSWERS.Component.prototype);
+MyCustomComponent.prototype.constructor = MyCustomComponent;
+MyCustomComponent.defaultTemplateName = function () { return 'default' };
+MyCustomComponent.areDuplicateNamesAllowed = function () { return false };
+Object.defineProperty(MyCustomComponent, 'type', { get: function () { return 'MyCustomComponent' } });
+
+ANSWERS.registerComponentType(MyCustomComponent); // Register the component with the library
+
+

Now you can use your custom component like any built-in component:

+
ANSWERS.addComponent('MyCustomComponent', {
+  container: '.my-component-container',
+  template: `<div>{{_config.myProperty}}</div>`,
+  myProperty: 'my property'
+});
+
+

Template Helpers

+

When using handlebars templates, Answers ships with a bunch of pre-built template helpers that you can use. You can learn more about them here.

+

If you want to register custom template helpers to the handlebars render, you can do so like this:

+
ANSWERS.registerHelper('noop', function(options) {
+  return options.fn(this);
 })
 
+

You can learn more about the interface for registering helpers by taking a look at the Handlebars Block Helpers documentation.

Analytics

-

Answers will track some basic interaction analytics automatically, such as search bar impressions and Call-To-Action clicks. You may add additional, custom analytic events to templates using certain data attributes, explained below.

-

Click Analytics

-

Click analytics can be attached to an element by adding the data-eventtype attribute to the element you want to track clicks for. The provided string should be the type of the analytics event. You can optionally include metadata inside the data-eventoptions attribute, in a JSON format. Whenever the element is clicked, an analtyics event with that data will be sent to the server.

+

If a businessId is supplied in the config, Answers will track some basic interaction analytics automatically, such as search bar impressions and Call-To-Action clicks.

+

If you would like to add custom analytics on top of the built-in ones, use the following:

+

Custom Analytics Using JavaScript

+

You may send analytics from external code with the below interface.

+
  const event = new ANSWERS.AnalyticsEvent('CUSTOM');
+  event.addOptions({ myData: 'data' });
+  ANSWERS.AnalyticsReporter.report(event)
+
+

Custom Analytics Using Data Attributes

+

You may add additional, custom analytic events to templates using certain data attributes. Click analytics can be attached to an element by adding the data-eventtype attribute to the element you want to track clicks for. The provided string should be the type of the analytics event. You can optionally include metadata inside the data-eventoptions attribute, in a JSON format. Whenever the element is clicked, an analtyics event with that data will be sent to the server.

<button class="driving-directions-button"
         data-eventtype="DRIVING_DIRECTIONS"
         data-eventoptions='{"store": "{{store}}"}'
 >
     Drive to {{store}}
 </button>
-
+
+

Built-In Analytics Events For CTAs

+

Here are the possible Event Types for CTAs:

+
    +
  • TITLE_CLICK
  • +
  • CTA_CLICK
  • +
  • TAP_TO_CALL
  • +
  • ORDER_NOW
  • +
  • ADD_TO_CART
  • +
  • APPLY_NOW
  • +
  • DRIVING_DIRECTIONS
  • +
  • VIEW_WEBSITE
  • +
  • EMAIL
  • +
  • BOOK_APPOINTMENT
  • +
  • RSVP
  • +
+

These types are accepted as the analytics attribute in Calls To Action.

+

Conversion Tracking

+

By default, Answers does not perform conversion tracking for analytics. To opt-in to this behavior, use the setConversionsOptIn method after initialization:

+
ANSWERS.init({ ... });
+agreementButton.onclick = function() { ANSWERS.setConversionsOptIn(true); };
+
+

You must also add the following to your HTML:

+
<script src="https://assets.sitescdn.net/ytag/ytag.min.js"></script>
+
+

On-Search Analytics

+

You can find instructions for configuring on search analytics above in these sections: onVerticalSearch Configuration, onUniversalSearch Configuration.

+

Rich Text Formatting

+

The Answers SDK exposes a formatRichText function which translates CommonMark to HTML. This function will +ensure that a Rich Text Formatted value is shown properly on the page. To use this function, call it like so:

+
ANSWERS.formatRichText(rtfFieldValue, eventOptionsFieldName, targetConfig)
+
+

For instance, this function can be used in the dataMappings of a Card to display an RTF attribute.

+

When clicking any link in the resultant HTML, an AnalyticsEvent will be fired. If the eventOptionsFieldName has been +specified, the eventOptions will include a fieldName attribute with the given value.

+

The targetConfig parameter dictates where the link is opened: the current window, a new tab, etc. It can have the following forms:

+
targetConfig = { url: '_blank', phone: '_self', email: '_parent' }
+targetConfig = '_blank'
+
+

When targetConfig is a string, it is assumed that any link, regardless of type, has the specified target behavior. This parameter, like eventOptionsFieldName, is optional. When not provided, no target attribute is supplied to the links.

+

Note that when using this function, you must ensure that the relevant Handlebars template correctly unescapes the output HTML.

+

CSS Variable Styling

+

The Answers SDK supports styling specific elements with CSS variables for runtime styling.

+
    +
  • All sdk variables are exposed in the :root ruleset
  • +
  • All overridden variables must be included the :root ruleset
  • +
  • All overrides should be defined after Answers CSS is imported and before ANSWERS.init is called
  • +
  • To see available variables, see the scss modules in the SDK
  • +
  • For example, to override:
  • +
+
<style>
+  :root {
+    --yxt-font-font-family: sans-serif;
+    --yxt-color-brand-primary: green;
+    --yxt-color-text-primary: #212121;
+    --yxt-searchbar-button-background-color-hover: red;
+    --yxt-nav-border-color: #e9e9e9;
+  }
+</style>
+
+

Most browsers have native css variable support. In legacy browsers, +we use the css-vars-ponyfill.

+
    +
  • The SDK will do automatic resolution of CSS variables on initialization. Variables should be loaded +before initialization. Overrided variables should be loaded after sdk variables are loaded.
  • +
  • You can opt-out with the disableCssVariablesPonyfill flag in ANSWERS.init.
  • +
  • If you opt-out of automatic resolution of variables, you should call ANSWERS.ponyfillCssVariables() +after css variables are loaded and before components are added.
  • +
  • If you change a css variable value after initialization and wish to see the change in variable +value in a legacy browser, you should call ANSWERS.ponyfillCssVariables() after the value is changed.
  • +
  • We support all callback functions, described here
  • +
+
ANSWERS.ponyfillCssVariables({
+        onError: function() {},
+        onSuccess: function() {},
+        onFinally: function() {},
+});
+
+

Processing Translations

+

The Answers SDK provides functionality to perform translation interpolation, pluralization, or both.

+

Interpolation

+

Interpolation allows the use of dynamic values in translations. Interpolation parameters are defined +inside double brackets, and they must also be defined in an object in the second parameter.

+

The following example will return 'Bonjour Howard' provided the variable myName equals 'Howard':

+
ANSWERS.processTranslation('Bonjour [[name]]', { name: myName });
+
+

The translation processor is also available though a Handlebars helper:

+
{{ processTranslation phrase='Bonjour [[name1]] et [[name2]]' name1=name1 name2=name2}}
+
+

Pluralization

+

Pluralization makes it possible to select the plural form of a translation depending on a +qualifying count. Different languages have different plural rules, which is the method of +selecting a plural form based on a count. An optional third parameter 'count' and an optional +fourth parameter 'locale' may be used for pluralization. The locale parameter determines the +plural forms and the plural rule used. If locale is not defined but the first +parameter is an object containing pluralizations, the locale supplied in the ANSWERS.init() will be +used. For more information on plural forms, see this doc:

+

The following example will use the singular form keyed by '0' for a count of 1, and the plural +form keyed by '1' for any other count. A count of 0 will return '0 results':

+
ANSWERS.processTranslation(
+  { 0: '[[resultsCount]] result', 1: '[[resultsCount]] results' }, 
+  { resultsCount: count }, 
+  count, 
+  'en'
+);
+
+

French is different than English in that a count of zero uses the same plural form as a count +of one. For example, a count of 0 will return '0 résultat':

+
ANSWERS.processTranslation(
+  { 0: '[[resultsCount]] résultat', 1: '[[resultsCount]] résultats' }, 
+  { resultsCount: count }, 
+  count, 
+  'fr'
+);
+
+

Here's what the usage looks like in a Handlebars helper:

+
{{ processTranslation 
+  pluralForm0='[[resultsCount]] résultat' 
+  pluralForm1='[[resultsCount]] résultats' 
+  resultsCount=count 
+  count=count 
+  locale='fr' 
+}}
+
+

Performance Metrics

+

The SDK uses the Performance API, via window.performance.mark(), to create performance metrics regarding ANSWERS.init(), vertical search, and universal search. These marks can be viewed through browser developer tools, or programmatically through window.performance.getEntries().

+

ANSWERS.init()

+
    +
  1. +

    'yext.answers.initStart' called when ANSWERS.init beings

    +
  2. +
  3. +

    'yext.answers.ponyfillStart' called when css-variables ponyfill starts

    +
  4. +
  5. +

    'yext.answers.ponyfillEnd' called when css-variables ponyfill ends.

    +
  6. +
  7. +

    'yext.answers.statusStart' called when the ANSWERS status call is made

    +
  8. +
  9. +

    'yext.answers.statusEnd' called when the ANSWERS status check is done

    +
  10. +
+

Vertical Search

+
    +
  1. +

    'yext.answers.verticalQueryStart' called when a vertical query begins

    +
  2. +
  3. +

    'yext.answers.verticalQuerySent' called right before the vertical query API call is made

    +
  4. +
  5. +

    'yext.answers.verticalQueryResponseReceived' called immediately after a vertical query response is received

    +
  6. +
  7. +

    'yext.answers.verticalQueryResponseRendered' called after a vertial query is finished, and all components have finished rendering

    +
  8. +
+

Universal Search

+
    +
  1. +

    'yext.answers.universalQueryStart' called when a universal query starts

    +
  2. +
  3. +

    'yext.answers.universalQuerySent' called right before the universal query API call is made

    +
  4. +
  5. +

    'yext.answers.universalQueryResponseReceived' called immediately after a universal query response is received

    +
  6. +
  7. +

    'yext.answers.universalQueryResponseRendered' called after a universal query is finished and all components have finished rendering

    +
  8. +
+

License

+

Yext Answers-Search-UI is an open-sourced library licensed under the BSD-3 License.

+

Third Party Licenses

+

The licenses of our 3rd party dependencies are collected here: THIRD-PARTY-NOTICES.

@@ -513,13 +2270,13 @@

Click Analytics


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/module-AccordionCardComponent.html b/docs/module-AccordionCardComponent.html new file mode 100644 index 000000000..f99c63407 --- /dev/null +++ b/docs/module-AccordionCardComponent.html @@ -0,0 +1,585 @@ + + + + + JSDoc: Module: AccordionCardComponent + + + + + + + + + + +
+ +

Module: AccordionCardComponent

+ + + + + + +
+ +
+ + + +
+ +
+
+ + + + + +
+ + + + + + + + + + + + + + +

Members

+ + + +

isExpanded :boolean

+ + + + +
+ Whether the accordion is collapsed or not. +Defaults to true only if the expanded option is true +and this is the first card in the results. +
+ + + +
Type:
+
    +
  • + +boolean + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

result :Result

+ + + + +
+ The result data, sent to children CTA Components that need this. +
+ + + +
Type:
+
    +
  • + +Result + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

verticalKey :string

+ + + + +
+ Vertical key for the card, added to analytics events sent by this component. +
+ + + +
Type:
+
    +
  • + +string + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

addChild()

+ + + + + + +
+ For passing functions to the config of children CTACollectionComponent +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

handleClick(toggleEl, accordionBodyEl, accordionEl)

+ + + + + + +
+ Click handler for the accordion toggle button +This is used over set state because it's a lot smoother, since +it doesn't rip the whole component off of the page and remount it. +Also reports an analytics event. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
toggleEl + + +HTMLElement + + + + the toggle element
accordionBodyEl + + +HTMLElement + + + + the .js-yxt-AccordionCard-body element
accordionEl + + +HTMLElement + + + + the root accordion element
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/docs/module-AccordionResultsComponent.html b/docs/module-AccordionResultsComponent.html new file mode 100644 index 000000000..c58577436 --- /dev/null +++ b/docs/module-AccordionResultsComponent.html @@ -0,0 +1,1451 @@ + + + + + JSDoc: Module: AccordionResultsComponent + + + + + + + + + + +
+ +

Module: AccordionResultsComponent

+ + + + + + +
+ +
+ + + +
+ +
+
+ + + + + +
+ + + + + + + + + + + + + + +

Members

+ + + +

(static) type

+ + + + +
+ the component type +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

_selectorBase :string

+ + + + +
+ base selector to use when finding DOM targets +
+ + + +
Type:
+
    +
  • + +string + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

collapsedClass :string

+ + + + +
+ collapsed state class +
+ + + +
Type:
+
    +
  • + +string + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

verticalConfigId :string|null

+ + + + +
+ vertical config id is required for analytics +
+ + + +
Type:
+
    +
  • + +string +| + +null + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

(static) defaultTemplateName() → {string}

+ + + + + + +
+ The template to render +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +string + + +
+
+ + + + + + + + + + + + + +

bodySelector() → {string}

+ + + + + + +
+ helper for the content element selector +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +string + + +
+
+ + + + + + + + + + + + + +

buildSelector(child) → {string}

+ + + + + + +
+ helper for composing child element selectors +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
child + + +string + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +string + + +
+
+ + + + + + + + + + + + + +

changeHeight(targetEl, wrapperEl)

+ + + + + + +
+ toggles the height between 0 and the content height for smooth animation +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
targetEl + + +HTMLElement + + + +
wrapperEl + + +HTMLElement + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

handleClick(wrapperEl, toggleEl, contentEl)

+ + + + + + +
+ click handler for the accordion toggle button +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
wrapperEl + + +HTMLElement + + + + the toggle container
toggleEl + + +HTMLElement + + + + the button
contentEl + + +HTMLElement + + + + the toggle target
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

isCollapsed(wrapperEl) → {boolean}

+ + + + + + +
+ returns true if the element is currently collapsed +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
wrapperEl + + +HTMLElement + + + + the toggle container
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +boolean + + +
+
+ + + + + + + + + + + + + +

onMount() → {AccordionResultsComponent}

+ + + + + + +
+ overrides onMount to add bindings to change the height on click +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +AccordionResultsComponent + + +
+
+ + + + + + + + + + + + + +

toggleSelector() → {string}

+ + + + + + +
+ helper for the toggle button selector +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +string + + +
+
+ + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/docs/module-AlternativeVerticals.html b/docs/module-AlternativeVerticals.html new file mode 100644 index 000000000..948a9c796 --- /dev/null +++ b/docs/module-AlternativeVerticals.html @@ -0,0 +1,325 @@ + + + + + JSDoc: Module: AlternativeVerticals + + + + + + + + + + +
+ +

Module: AlternativeVerticals

+ + + + + + +
+ +
+ + + +
+ +
+
+ + + + + +
+ + + + + + + + + + + + + + +

Members

+ + + +

alternativeVerticals :Section

+ + + + +
+ Alternative verticals that have results for the current query +
+ + + +
Type:
+
    +
  • + +Section + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

(static) fromCore(alternativeVerticals, formatters)

+ + + + + + +
+ Create alternative verticals from server data +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
alternativeVerticals + + +Array.<Object> + + + +
formatters + + +Object.<string, function()> + + + + applied to the result fields
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/docs/module-AlternativeVerticalsComponent.html b/docs/module-AlternativeVerticalsComponent.html new file mode 100644 index 000000000..45f2096d1 --- /dev/null +++ b/docs/module-AlternativeVerticalsComponent.html @@ -0,0 +1,912 @@ + + + + + JSDoc: Module: AlternativeVerticalsComponent + + + + + + + + + + +
+ +

Module: AlternativeVerticalsComponent

+ + + + + + +
+ +
+ + + +
+ +
+
+ + + + + +
+ + + + + + + + + + + + + + +

Members

+ + + +

_baseUniversalUrl :string|null

+ + + + +
+ The url to the universal page to link back to without query params +
+ + + +
Type:
+
    +
  • + +string +| + +null + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

_currentVerticalLabel :string

+ + + + +
+ The name of the vertical that is exposed for the link +
+ + + +
Type:
+
    +
  • + +string + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

_isShowingResults :boolean

+ + + + +
+ Whether or not results are displaying, used to control language in the info box +
+ + + +
Type:
+
    +
  • + +boolean + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

_universalUrl :string|null

+ + + + +
+ The url to the universal page to link back to with current query params +
+ + + +
Type:
+
    +
  • + +string +| + +null + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

verticalSuggestions :Array.<AlternativeVertical>

+ + + + +
+ The alternative vertical search suggestions, parsed from alternative verticals and +the global verticals config. +This gets updated based on the server results +
+ + + +
Type:
+
    +
  • + +Array.<AlternativeVertical> + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

(static) defaultTemplateName() → {string}

+ + + + + + +
+ The template to render +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +string + + +
+
+ + + + + + + + + + + + + +

_buildVerticalSuggestions(alternativeVerticals, verticalsConfig)

+ + + + + + +
+ _buildVerticalSuggestions will construct an array of {AlternativeVertical} +from alternative verticals and verticalPages configuration +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
alternativeVerticals + + +object + + + + alternativeVerticals server response
verticalsConfig + + +object + + + + the configuration to use
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

_getUniversalURL(baseUrl, params) → {string}

+ + + + + + +
+ Adds parameters that are dynamically set. Removes parameters for facets, +filters, and pagination, which should not persist across the experience. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
baseUrl + + +string + + + + The url append the appropriate params to. Note: + params already on the baseUrl will be stripped
params + + +SearchParams + + + + The parameters to include in the experience URL
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ The formatted experience URL with appropriate query params +
+ + + +
+
+ Type +
+
+ +string + + +
+
+ + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/docs/module-Analytics.html b/docs/module-Analytics.html deleted file mode 100644 index 43dfd02a3..000000000 --- a/docs/module-Analytics.html +++ /dev/null @@ -1,170 +0,0 @@ - - - - - JSDoc: Class: module:Analytics - - - - - - - - - - -
- -

Class: module:Analytics

- - - - - - -
- -
- -

module:Analytics()

- -
Interface for reporting Analytics
- - -
- -
-
- - - - -

Constructor

- - - -

new module:Analytics()

- - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - - -
- -
- Documentation generated by JSDoc 3.6.2 on Wed Jul 03 2019 10:34:41 GMT-0400 (Eastern Daylight Time) -
- - - - - \ No newline at end of file diff --git a/docs/module-AnalyticsReporter.html b/docs/module-AnalyticsReporter.html index 378bac4f7..4272b2f85 100644 --- a/docs/module-AnalyticsReporter.html +++ b/docs/module-AnalyticsReporter.html @@ -30,7 +30,7 @@

Class: module:AnalyticsReporter

module:AnalyticsReporter()

-
Class for reporting analytics events to the server
+
Class for reporting analytics events to the server via HTTP
@@ -77,6 +77,13 @@

Implements: +
    + +
  • AnalyticsReporterService
  • + +
+ @@ -93,7 +100,7 @@

Source:
@@ -139,7 +146,255 @@

Members

+ + + +

_businessId :number

+ + + + +
+ The internal business identifier used for reporting +
+ + + +
Type:
+
    +
  • + +number + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

report()

+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

setConversionTrackingEnabled()

+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + @@ -155,13 +410,13 @@


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/module-ApiRequest.html b/docs/module-ApiRequest.html index c0f3b879e..85a1b7074 100644 --- a/docs/module-ApiRequest.html +++ b/docs/module-ApiRequest.html @@ -94,7 +94,7 @@

new
Source:
@@ -150,7 +150,7 @@

Methods

-

get() → {Promise}

+

get(opts) → {Promise.<Response>}

@@ -169,6 +169,206 @@

get + + + + Name + + + Type + + + + + + Description + + + + + + + + + opts + + + + + +Object + + + + + + + + + + Any configuration options to use for the GET request. + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Promise.<Response> + + +
+
+ + + + + + + + + + + + + +

post(opts) → {Promise.<Response>}

+ + + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +Object + + + +
+ + @@ -202,7 +402,7 @@

getSource:
@@ -238,7 +438,7 @@
Returns:
-Promise +Promise.<Response>
@@ -266,13 +466,13 @@
Returns:

- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/module-AppliedFiltersComponent.html b/docs/module-AppliedFiltersComponent.html new file mode 100644 index 000000000..edfd04e99 --- /dev/null +++ b/docs/module-AppliedFiltersComponent.html @@ -0,0 +1,3359 @@ + + + + + JSDoc: Module: AppliedFiltersComponent + + + + + + + + + + +
+ +

Module: AppliedFiltersComponent

+ + + + + + +
+ +
+ + + +
+ +
+
+ + + + + +
+ + + + + + + + + + + + + + +

Members

+ + + +

_compiledResultsCountTemplate :function

+ + + + +
+ The compiled custom results count template, if the user specifies one. +
+ + + +
Type:
+
    +
  • + +function + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

nlpFilterNodes :Array.<AppliedQueryFilter>

+ + + + +
+ Array of nlp filters in the search response. +
+ + + +
Type:
+
    +
  • + +Array.<AppliedQueryFilter> + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

resultsCount :number

+ + + + +
+ Total number of results. +
+ + + +
Type:
+
    +
  • + +number + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

resultsLength :number

+ + + + +
+ Number of results displayed on the page. +
+ + + +
Type:
+
    +
  • + +number + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

(static) defaultTemplateName() → {string}

+ + + + + + +
+ The template to render +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +string + + +
+
+ + + + + + + + + + + + + +

(static) defaultTemplateName() → {string}

+ + + + + + +
+ The template to render +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +string + + +
+
+ + + + + + + + + + + + + +

_calculateAppliedFilterNodes()

+ + + + + + +
+ Pulls applied filter nodes from FilterRegistry, then retrives an array of +the leaf nodes, and then removes hidden or empty FilterNodes. Then appends +the currently applied nlp filters. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

_calculateAppliedFilterNodes()

+ + + + + + +
+ Pulls applied filter nodes from FilterRegistry, then retrives an array of +the leaf nodes, and then removes hidden or empty FilterNodes. Then appends +the currently applied nlp filters. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

_createAppliedFiltersArray() → {Array.<Object>}

+ + + + + + +
+ Returns an array of object the handlebars can understand and render +the applied filters bar from. Our handlebars can only loop through arrays, +not objects, so we need to reformat the grouped applied filters. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Array.<Object> + + +
+
+ + + + + + + + + + + + + +

_createAppliedFiltersArray() → {Array.<Object>}

+ + + + + + +
+ Returns an array of object the handlebars can understand and render +the applied filters bar from. Our handlebars can only loop through arrays, +not objects, so we need to reformat the grouped applied filters. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Array.<Object> + + +
+
+ + + + + + + + + + + + + +

_getPrunedNlpFilterNodes() → {Array.<FilterNode>}

+ + + + + + +
+ Returns the currently applied nlp filter nodes, with nlp filter nodes that +are duplicates of other filter nodes removed or filter on hiddenFields removed. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Array.<FilterNode> + + +
+
+ + + + + + + + + + + + + +

_getPrunedNlpFilterNodes() → {Array.<FilterNode>}

+ + + + + + +
+ Returns the currently applied nlp filter nodes, with nlp filter nodes that +are duplicates of other filter nodes removed or filter on hiddenFields removed. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Array.<FilterNode> + + +
+
+ + + + + + + + + + + + + +

_groupAppliedFilters() → {Array.<Object>}

+ + + + + + +
+ Combine all of the applied filters into a format the handlebars +template can work with. +Keys are the fieldName of the filter. Values are an array of objects with a +displayValue and dataFilterId. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Array.<Object> + + +
+
+ + + + + + + + + + + + + +

_groupAppliedFilters() → {Array.<Object>}

+ + + + + + +
+ Combine all of the applied filters into a format the handlebars +template can work with. +Keys are the fieldName of the filter. Values are an array of objects with a +displayValue and dataFilterId. +TODO (SPR-2350): give every node a unique id, and use that instead of index for +dataFilterId. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Array.<Object> + + +
+
+ + + + + + + + + + + + + +

_removeFilterTag(tag)

+ + + + + + +
+ Call remove callback for the FilterNode corresponding to a specific +removable filter tag. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
tag + + +HTMLElement + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

_removeFilterTag(tag)

+ + + + + + +
+ Call remove callback for the FilterNode corresponding to a specific +removable filter tag. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
tag + + +HTMLElement + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + +
+ +
+ + + +
+ +
+
+ + + + + +
+ + + + + + + + + + + + + + +

Members

+ + + +

_compiledResultsCountTemplate :function

+ + + + +
+ The compiled custom results count template, if the user specifies one. +
+ + + +
Type:
+
    +
  • + +function + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

nlpFilterNodes :Array.<AppliedQueryFilter>

+ + + + +
+ Array of nlp filters in the search response. +
+ + + +
Type:
+
    +
  • + +Array.<AppliedQueryFilter> + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

resultsCount :number

+ + + + +
+ Total number of results. +
+ + + +
Type:
+
    +
  • + +number + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

resultsLength :number

+ + + + +
+ Number of results displayed on the page. +
+ + + +
Type:
+
    +
  • + +number + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

(static) defaultTemplateName() → {string}

+ + + + + + +
+ The template to render +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +string + + +
+
+ + + + + + + + + + + + + +

(static) defaultTemplateName() → {string}

+ + + + + + +
+ The template to render +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +string + + +
+
+ + + + + + + + + + + + + +

_calculateAppliedFilterNodes()

+ + + + + + +
+ Pulls applied filter nodes from FilterRegistry, then retrives an array of +the leaf nodes, and then removes hidden or empty FilterNodes. Then appends +the currently applied nlp filters. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

_calculateAppliedFilterNodes()

+ + + + + + +
+ Pulls applied filter nodes from FilterRegistry, then retrives an array of +the leaf nodes, and then removes hidden or empty FilterNodes. Then appends +the currently applied nlp filters. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

_createAppliedFiltersArray() → {Array.<Object>}

+ + + + + + +
+ Returns an array of object the handlebars can understand and render +the applied filters bar from. Our handlebars can only loop through arrays, +not objects, so we need to reformat the grouped applied filters. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Array.<Object> + + +
+
+ + + + + + + + + + + + + +

_createAppliedFiltersArray() → {Array.<Object>}

+ + + + + + +
+ Returns an array of object the handlebars can understand and render +the applied filters bar from. Our handlebars can only loop through arrays, +not objects, so we need to reformat the grouped applied filters. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Array.<Object> + + +
+
+ + + + + + + + + + + + + +

_getPrunedNlpFilterNodes() → {Array.<FilterNode>}

+ + + + + + +
+ Returns the currently applied nlp filter nodes, with nlp filter nodes that +are duplicates of other filter nodes removed or filter on hiddenFields removed. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Array.<FilterNode> + + +
+
+ + + + + + + + + + + + + +

_getPrunedNlpFilterNodes() → {Array.<FilterNode>}

+ + + + + + +
+ Returns the currently applied nlp filter nodes, with nlp filter nodes that +are duplicates of other filter nodes removed or filter on hiddenFields removed. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Array.<FilterNode> + + +
+
+ + + + + + + + + + + + + +

_groupAppliedFilters() → {Array.<Object>}

+ + + + + + +
+ Combine all of the applied filters into a format the handlebars +template can work with. +Keys are the fieldName of the filter. Values are an array of objects with a +displayValue and dataFilterId. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Array.<Object> + + +
+
+ + + + + + + + + + + + + +

_groupAppliedFilters() → {Array.<Object>}

+ + + + + + +
+ Combine all of the applied filters into a format the handlebars +template can work with. +Keys are the fieldName of the filter. Values are an array of objects with a +displayValue and dataFilterId. +TODO (SPR-2350): give every node a unique id, and use that instead of index for +dataFilterId. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Array.<Object> + + +
+
+ + + + + + + + + + + + + +

_removeFilterTag(tag)

+ + + + + + +
+ Call remove callback for the FilterNode corresponding to a specific +removable filter tag. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
tag + + +HTMLElement + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

_removeFilterTag(tag)

+ + + + + + +
+ Call remove callback for the FilterNode corresponding to a specific +removable filter tag. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
tag + + +HTMLElement + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/docs/module-AutoComplete.html b/docs/module-AutoComplete.html deleted file mode 100644 index b8aca8a9f..000000000 --- a/docs/module-AutoComplete.html +++ /dev/null @@ -1,311 +0,0 @@ - - - - - JSDoc: Class: module:AutoComplete - - - - - - - - - - -
- -

Class: module:AutoComplete

- - - - - - -
- -
- -

module:AutoComplete()

- -
A wrapper around the AutoComplete {ApiRequest} endpoints
- - -
- -
-
- - - - -

Constructor

- - - -

new module:AutoComplete()

- - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - -

Methods

- - - - - - - -

queryFilter(input)

- - - - - - -
- Autocomplete filters -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
input - - -string - - - - The input to use for auto complete
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - - -
- -
- Documentation generated by JSDoc 3.6.2 on Wed Jul 03 2019 10:34:41 GMT-0400 (Eastern Daylight Time) -
- - - - - \ No newline at end of file diff --git a/docs/module-AutoCompleteApi.html b/docs/module-AutoCompleteApi.html deleted file mode 100644 index 0018db99e..000000000 --- a/docs/module-AutoCompleteApi.html +++ /dev/null @@ -1,312 +0,0 @@ - - - - - JSDoc: Class: module:AutoCompleteApi - - - - - - - - - - -
- -

Class: module:AutoCompleteApi

- - - - - - -
- -
- -

module:AutoCompleteApi()

- -
AutoCompleteApi exposes an interface for network related matters -for all the autocomplete endpoints.
- - -
- -
-
- - - - -

Constructor

- - - -

new module:AutoCompleteApi()

- - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - -

Methods

- - - - - - - -

queryFilter(input)

- - - - - - -
- Autocomplete filters -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
input - - -string - - - - The input to use for auto complete
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - - -
- -
- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) -
- - - - - \ No newline at end of file diff --git a/docs/module-AutoCompleteComponent.html b/docs/module-AutoCompleteComponent.html index c6ad6e5a6..fc6e21dc7 100644 --- a/docs/module-AutoCompleteComponent.html +++ b/docs/module-AutoCompleteComponent.html @@ -102,7 +102,7 @@

(static) typeSource:
@@ -120,13 +120,13 @@

(static) type_barKey :string

+

_autocompleteEls :string

- The `barKey` in the vertical search to use for auto-complete + A selector for the autocomplete elementes
@@ -174,7 +174,493 @@
Type:
Source:
+ + + + + + + + + + + + + + + + +

_autoFocus :boolean

+ + + + +
+ Whether the input is autocomatically focused or not +
+ + + +
Type:
+
    +
  • + +boolean + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

_inputEl :string

+ + + + +
+ A reference to the input el selector for auto complete +
+ + + +
Type:
+
    +
  • + +string + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

_isOpen :boolean

+ + + + +
+ Indicates the initial open/closed status of this component +
+ + + +
Type:
+
    +
  • + +boolean + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

_onChange

+ + + + +
+ Callback invoked when keys are used to navigate through the auto complete. Note that this is +not called when either the `Enter` key is pressed or the mouse is used to select an +autocomplete option. +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

_onClose :function

+ + + + +
+ Callback invoked when the autocomplete component changes from open to closed. +
+ + + +
Type:
+
    +
  • + +function + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

_onOpen :function

+ + + + +
+ Callback invoked when the autocomplete component changes from closed to open. +
+ + + +
Type:
+
    +
  • + +function + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

_onSubmit

+ + + + +
+ Callback invoked when the `Enter` key is pressed on auto complete. +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
@@ -192,13 +678,15 @@
Type:
-

_inputEl :string

+

_originalQuery :string

- A reference to the input el selector for auto complete + An internal reference to the input value when typing. +We use this for resetting the state of the input value when other interactions (e.g. result navigation) +change based on interactions. For instance, hitting escape should reset the value to the original typed query.
@@ -246,7 +734,7 @@
Type:
Source:
@@ -264,17 +752,28 @@
Type:
-

_onSubmit

+

_resultIndex :number

- Callback invoked when the `Enter` key is pressed on auto complete. + Used for keyboard navigation through results. +An internal reference to the current result index we're navigating on.
+
Type:
+
    +
  • + +number + + +
  • +
+ @@ -308,7 +807,7 @@

_onSubmitSource:
@@ -326,15 +825,14 @@

_onSubmit_originalQuery :string

+

_sectionIndex :number

- An internal reference to the input value when typing. -We use this for resetting the state of the input value when other interactions (e.g. result navigation) -change based on interactions. For instance, hitting escape should reset the value to the original typed query. + Used for keyboard navigation through results. +An internal reference to the current section we're navigating in.
@@ -343,7 +841,7 @@
Type:

+ + + + + + + + + + + + + + + + + + + + + + + + + + +

hasResults()

+ + + + + + +
+ returns true if we have results in any section +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
@@ -994,6 +1612,16 @@

closeReturns:

+ + +
+ boolean +
+ + + + @@ -1058,7 +1686,7 @@

onCreateSource:
@@ -1147,7 +1775,7 @@

resetSource:
@@ -1237,7 +1865,7 @@

setStateSource:
@@ -1375,7 +2003,7 @@
Parameters:
Source:
@@ -1463,7 +2091,7 @@

updateStat
Source:
@@ -1509,13 +2137,13 @@

updateStat
- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/module-AutoCompleteData.html b/docs/module-AutoCompleteData.html index 162954195..5e4c23550 100644 --- a/docs/module-AutoCompleteData.html +++ b/docs/module-AutoCompleteData.html @@ -70,13 +70,13 @@

Module: AutoCompleteData


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/module-AutoCompleteDataTransformer.html b/docs/module-AutoCompleteDataTransformer.html deleted file mode 100644 index de028e39b..000000000 --- a/docs/module-AutoCompleteDataTransformer.html +++ /dev/null @@ -1,174 +0,0 @@ - - - - - JSDoc: Class: module:AutoCompleteDataTransformer - - - - - - - - - - -
- -

Class: module:AutoCompleteDataTransformer

- - - - - - -
- -
- -

module:AutoCompleteDataTransformer()

- -
A Data Transformer that takes the response object from a AutoComplete request -And transforms in to a front-end oriented data structure that our -component library and core storage understand. - -TODO(billy) Create our own front-end data models
- - -
- -
-
- - - - -

Constructor

- - - -

new module:AutoCompleteDataTransformer()

- - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - - -
- -
- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) -
- - - - - \ No newline at end of file diff --git a/docs/module-CTACollectionComponent.html b/docs/module-CTACollectionComponent.html new file mode 100644 index 000000000..67e8b08e7 --- /dev/null +++ b/docs/module-CTACollectionComponent.html @@ -0,0 +1,651 @@ + + + + + JSDoc: Module: CTACollectionComponent + + + + + + + + + + +
+ +

Module: CTACollectionComponent

+ + + + + + +
+ +
+ + + +
+ +
+
+ + + + + +
+ + + + + + + + + + + + + + +

Members

+ + + +

callsToAction :Array.<Object>

+ + + + +
+ The config for each calls to action component to render. +
+ + + +
Type:
+
    +
  • + +Array.<Object> + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

includeLegacyClasses :boolean

+ + + + +
+ Whether the DOM should include legacy class names +
+ + + +
Type:
+
    +
  • + +boolean + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

isUniversal :boolean

+ + + + +
+ Whether this cta is part of a universal search. +
+ + + +
Type:
+
    +
  • + +boolean + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

result :Result

+ + + + +
+ Result data +
+ + + +
Type:
+
    +
  • + +Result + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

verticalKey :string

+ + + + +
+ Vertical key for the search. +
+ + + +
Type:
+
    +
  • + +string + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

(static) resolveCTAMapping(result, …ctas) → {Array.<Object>}

+ + + + + + +
+ Handles resolving ctas from a cta mapping which are either +1. a function that returns a cta's config +2. an object that has a per-attribute mapping of either a + a) static value + b) function that takes in resut data and returns the given attributes value +Note: Intentionally does not allow nesting functions. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
result + + +Object + + + + + + + + + +
ctas + + + + + + + + <repeatable>
+ +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Array.<Object> + + +
+
+ + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:12 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/docs/module-CTAComponent.html b/docs/module-CTAComponent.html new file mode 100644 index 000000000..b38a6403c --- /dev/null +++ b/docs/module-CTAComponent.html @@ -0,0 +1,85 @@ + + + + + JSDoc: Module: CTAComponent + + + + + + + + + + +
+ +

Module: CTAComponent

+ + + + + + +
+ +
+ + + +
+ +
+
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:12 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/docs/module-CardComponent.html b/docs/module-CardComponent.html new file mode 100644 index 000000000..568e706cf --- /dev/null +++ b/docs/module-CardComponent.html @@ -0,0 +1,669 @@ + + + + + JSDoc: Module: CardComponent + + + + + + + + + + +
+ +

Module: CardComponent

+ + + + + + +
+ +
+ + + +
+ +
+
+ + + + + +
+ + + + + + + + + + + + + + +

Members

+ + + +

result :Result

+ + + + +
+ The result data for this card. +
+ + + +
Type:
+
    +
  • + +Result + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

verticalKey :string

+ + + + +
+ Vertical key for the search. +
+ + + +
Type:
+
    +
  • + +string + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

(static) applyDataMappings(result, dataMappings)

+ + + + + + +
+ Used by children card components like StandardCardComponent to +apply given template mappings as config. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
result + + +Result + + + +
dataMappings + + +Object +| + +function + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(static) defaultTemplateName() → {string}

+ + + + + + +
+ The template to render +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +string + + +
+
+ + + + + + + + + + + + + +

_handleRtfClickAnalytics(event, fieldName)

+ + + + + + +
+ A click handler for links in a Rich Text attriubte. When such a link is +clicked, an AnalyticsEvent needs to be fired. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
event + + +MouseEvent + + + + The click event.
fieldName + + +string + + + + The name of the Rich Text field used in the + attriubte.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/docs/module-CombinedFilterNode.html b/docs/module-CombinedFilterNode.html new file mode 100644 index 000000000..aa7bf3027 --- /dev/null +++ b/docs/module-CombinedFilterNode.html @@ -0,0 +1,815 @@ + + + + + JSDoc: Class: module:CombinedFilterNode + + + + + + + + + + +
+ +

Class: module:CombinedFilterNode

+ + + + + + +
+ +
+ +

module:CombinedFilterNode()

+ +
A CombinedFilterNode represents a combined filter. +A combined filter is a set of filters combined with a FilterCombinators +($and or $or). Since a combined filter is just a set of other filters, +it does not have its own FilterMetadata, and its filter is dervied from +its children.
+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new module:CombinedFilterNode()

+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +

Members

+ + + +

children :Array.<FilterNode>

+ + + + + + +
Type:
+
    +
  • + +Array.<FilterNode> + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

combinator :string

+ + + + + + +
Type:
+
    +
  • + +string + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

getChildren() → {Array.<FilterNode>}

+ + + + + + +
+ Returns this node's children. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Array.<FilterNode> + + +
+
+ + + + + + + + + + + + + +

getFilter()

+ + + + + + +
+ Returns the filter created by combining this node's children. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

getMetadata() → {null}

+ + + + + + +
+ Returns the metadata associated with this node's filter. +Because a combined filter's purpose is solely to join together other filters, +and does not have its own filter, this value is always null. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +null + + +
+
+ + + + + + + + + + + + + +

getSimpleDescendants() → {Array.<SimpleFilterNode>}

+ + + + + + +
+ Recursively get all of the leaf SimpleFilterNodes. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Array.<SimpleFilterNode> + + +
+
+ + + + + + + + + + + + + +

remove()

+ + + + + + +
+ Removes this filter node from the FilterRegistry by calling remove on each of its +child FilterNodes. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/docs/module-Component.html b/docs/module-Component.html index 94b824507..f6e1cba77 100644 --- a/docs/module-Component.html +++ b/docs/module-Component.html @@ -97,7 +97,7 @@

new m
Source:
@@ -201,7 +201,7 @@

Type:
Source:
@@ -225,7 +225,8 @@

_className<
- A custom class to be applied to {this._container} node + A custom class to be applied to {this._container} node. Note that the class +'yxt-Answers-component' will be included as well.
@@ -273,7 +274,79 @@

Type:
Source:
+ + + + + + + +

+ + + + + + + + +

_config :Object

+ + + + +
+ Cache the options so that we can propogate properly to child components +
+ + + +
Type:
+
    +
  • + +Object + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
@@ -345,7 +418,7 @@
Type:
Source:
@@ -363,13 +436,13 @@
Type:
-

_opts :Object

+

_parentContainer :Component

- Cache the options so that we can propogate properly to child components + A local reference to the parent component, if exists
@@ -378,7 +451,7 @@
Type:
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

printVerbose :boolean

+ + + + +
+ If true, print entire error objects to the console for inspection +
+ + + +
Type:
+
    +
  • + +boolean + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

sendToServer :boolean

+ + + + +
+ If true, report the error the server for logging and monitoring +
+ + + +
Type:
+

+ +
+ + + + + + + +
+ +
+ +

exports

+ +
ErrorReporterService exposes an interface for reporting errors to the console +and to a backend
+ + +
+ +
+
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + +
+ + + + + + + + + + + + + + +

Members

+ + + +

_geolocationOptions :Object

+ + + + +
+ Options to pass to the geolocation api. +
+ + + +
Type:
+
    +
  • + +Object + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

_geolocationTimeoutAlert :Object

+ + + + +
+ Options for the geolocation timeout alert. +
+ + + +
Type:
+
    +
  • + +Object + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

_updateLocationEl :string

+ + + + +
+ The element used for updating location +Optionally provided. +
+ + + +
Type:
+
    +
  • + +string + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

_verticalKey :string

+ + + + +
+ The optional vertical key for vertical search configuration +If not provided, when location updated, +a universal search will be triggered. +
+ + + +
Type:
+
    +
  • + +string + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

_visibleForNoResults :boolean

+ + + + +
+ When the page is in a No Results state, whether to display the +vertical results count. +
+ + + +
Type:
+
    +
  • + +boolean + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

complexContents

+ + + + +
+ if not using a path, a the markup for a complex SVG +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

contents

+ + + + +
+ actual contents used +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

eventType :string

+ + + + +
+ The type of event to report +
+ + + +
Type:
+
    +
  • + +string + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

hasIcon :string

+ + + + +
+ Whether the vertical has an icon +
+ + + +
Type:
+
    +
  • + +string + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

iconName :string

+ + + + +
+ name of an icon from the default icon set +
+ + + +
Type:
+
    +
  • + +string + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

iconUrl :string

+ + + + +
+ URL of an icon +
+ + + +
Type:
+
    +
  • + +string + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

label :string

+ + + + +
+ The name of the vertical that is exposed for the link +
+ + + +
Type:
+
    +
  • + +string + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

listeners :Array.<StorageListener>

+ + + + +
+ The listeners to apply on changes to storage +
+ + + +
Type:
+
    +
  • + +Array.<StorageListener> + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

middleware

+ + + + +
+ Middleware functions to be run before a search. Can be either async or sync. +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

name

+ + + + +
+ the name of the icon +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

path

+ + + + +
+ an svg path definition +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

persistedStateListeners

+ + + + +
+ The listeners for changes in state (persistent storage changes) +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

persistedValueParser :function

+ + + + +
+ A hook for parsing values from persistent storage on init. +
+ + + +
Type:
+
    +
  • + +function + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

persistentStorage :DefaultPersistentStorage

+ + + + +
+ The persistent storage implementation to store state +across browser sessions and URLs +
+ + + +
Type:
+
    +
  • + +DefaultPersistentStorage + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

resultsCount :number

+ + + + +
+ The number of results to display next to each alternative +vertical +
+ + + +
Type:
+
    +
  • + +number + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + -

Constructor

+
Source:
+
-

new exports()

+ +
+ + + + + +

storage :Map.<string, *>

+ +
+ The core data for the storage +
+ +
Type:
+
    +
  • + +Map.<string, *> +
  • +
@@ -93,7 +3412,7 @@

new exportsSource:
@@ -109,22 +3428,42 @@

new exportsurl :string

+ + + + +
+ The complete URL, including the params +
+
Type:
+
    +
  • + +string +
  • +
+
+ + + - @@ -140,6 +3479,268 @@

new exportsSource: +
+ + + + + + + +

+ + + + + + + + +

viewBox :string

+ + + + +
+ the view box definition, defaults to 24x24 +
+ + + +
Type:
+
    +
  • + +string + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

popListener(queryParamsMap, queryParamsString)

+ + + + + + +
+ The listener for window.pop in the persistent storage +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
queryParamsMap + + +Map.<string, string> + + + + A Map containing the persisted state, + for example a map of 'query' => 'virginia'
queryParamsString + + +string + + + + the url params of the persisted state + for the above case 'query=virginia'
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + @@ -155,13 +3756,13 @@

new exports
- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/module.exports_module.exports.html b/docs/module.exports_module.exports.html new file mode 100644 index 000000000..caee404a7 --- /dev/null +++ b/docs/module.exports_module.exports.html @@ -0,0 +1,308 @@ + + + + + JSDoc: Class: exports + + + + + + + + + + +
+ +

Class: exports

+ + + + + + +
+ +
+ +

exports(config)

+ + +
+ +
+
+ + + + + + +

new exports(config)

+ + + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
config + + +
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
name + +
path + +
complexContents + +
viewBox + +
+ +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + \ No newline at end of file diff --git a/docs/ui_components_cards_accordioncardcomponent.js.html b/docs/ui_components_cards_accordioncardcomponent.js.html new file mode 100644 index 000000000..a6953028e --- /dev/null +++ b/docs/ui_components_cards_accordioncardcomponent.js.html @@ -0,0 +1,236 @@ + + + + + JSDoc: Source: ui/components/cards/accordioncardcomponent.js + + + + + + + + + + +
+ +

Source: ui/components/cards/accordioncardcomponent.js

+ + + + + + +
+
+
/** @module AccordionCardComponent */
+
+import Component from '../component';
+import CardComponent from './cardcomponent';
+import { cardTemplates, cardTypes } from './consts';
+import DOM from '../../dom/dom';
+import AnalyticsEvent from '../../../core/analytics/analyticsevent';
+import CTACollectionComponent from '../ctas/ctacollectioncomponent';
+
+class AccordionCardConfig {
+  constructor (config = {}) {
+    Object.assign(this, config);
+
+    const data = config.data || {};
+
+    /**
+     * The result data
+     * @type {Result}
+     */
+    const result = data.result || {};
+
+    /**
+     * The raw profile data
+     * @type {Object}
+     */
+    const rawResult = result._raw || {};
+
+    /**
+     * The dataMappings attribute of the config
+     * is either a function that returns additional config for
+     * a card or an object that is the additional config.
+     */
+    const dataMappings = config.dataMappings || {};
+    Object.assign(this, CardComponent.applyDataMappings(rawResult, dataMappings));
+
+    /**
+     * Vertical key for the card, added to analytics events sent by this component.
+     * @type {string}
+     */
+    this.verticalKey = config.verticalKey;
+
+    /**
+     * @type {string}
+     */
+    this.title = this.title || result.title || rawResult.name || '';
+
+    /**
+     * @type {string}
+     */
+    this.subtitle = this.subtitle;
+
+    /**
+     * @type {string}
+     */
+    this.details = this.details === null ? null : (this.details || result.details || rawResult.description || '');
+
+    /**
+     * If expanded is true the first accordion in vertical/universal results renders on page load expanded.
+     * @type {boolean}
+     */
+    this.expanded = this.expanded || false;
+
+    /**
+     * Either a function that spits out an array of CTA config objects or an array of CTA config objects
+     * or api fieldnames
+     * @type {Function|Array<Object|string>}
+     */
+    this.callsToAction = this.callsToAction || [];
+
+    /**
+     * Whether this card is part of a universal search. Used in analytics.
+     * @type {boolean}
+     */
+    this.isUniversal = config.isUniversal || false;
+  }
+}
+
+export default class AccordionCardComponent extends Component {
+  constructor (config = {}, systemConfig = {}) {
+    super(new AccordionCardConfig(config), systemConfig);
+
+    /**
+     * Whether the accordion is collapsed or not.
+     * Defaults to true only if the expanded option is true
+     * and this is the first card in the results.
+     * @type {boolean}
+     */
+    this.isExpanded = this._config.expanded && config._index === 0;
+
+    /**
+     * @type {Object}
+     */
+    const data = config.data || {};
+
+    /**
+     * Vertical key for the card, added to analytics events sent by this component.
+     * @type {string}
+     */
+    this.verticalKey = data.verticalKey;
+
+    /**
+     * The result data, sent to children CTA Components that need this.
+     * @type {Result}
+     */
+    this.result = data.result || {};
+  }
+
+  setState (data) {
+    const id = this.result.id || this.result.ordinal;
+    return super.setState({
+      ...data,
+      result: this.result,
+      isExpanded: this.isExpanded,
+      id: `${this.name}-${id}-${this.verticalKey}`,
+      hasCTAs: CTACollectionComponent.hasCTAs(this.result._raw, this._config.callsToAction)
+    });
+  }
+
+  /**
+   * Click handler for the accordion toggle button
+   * This is used over set state because it's a lot smoother, since
+   * it doesn't rip the whole component off of the page and remount it.
+   * Also reports an analytics event.
+   * @param {HTMLElement} toggleEl the toggle element
+   * @param {HTMLElement} accordionBodyEl the .js-yxt-AccordionCard-body element
+   * @param {HTMLElement} accordionEl the root accordion element
+   */
+  handleClick (toggleEl, accordionBodyEl, accordionEl) {
+    this.isExpanded = !this.isExpanded;
+    accordionEl.classList.toggle('yxt-AccordionCard--expanded');
+
+    accordionBodyEl.style.height = `${this.isExpanded ? accordionBodyEl.scrollHeight : 0}px`;
+
+    toggleEl.setAttribute('aria-expanded', this.isExpanded ? 'true' : 'false');
+    accordionBodyEl.setAttribute('aria-hidden', this.isExpanded ? 'false' : 'true');
+    const event = new AnalyticsEvent(this.isExpanded ? 'ROW_EXPAND' : 'ROW_COLLAPSE')
+      .addOptions({
+        verticalKey: this.verticalKey,
+        entityId: this.result._raw.id,
+        searcher: this._config.isUniversal ? 'UNIVERSAL' : 'VERTICAL'
+      });
+    this.analyticsReporter.report(event);
+  }
+
+  onMount () {
+    if (this._config.details) {
+      const toggleEl = DOM.query(this._container, '.js-yxt-AccordionCard-toggle');
+      const accordionBodyEl = DOM.query(this._container, '.js-yxt-AccordionCard-body');
+      const accordionEl = DOM.query(this._container, '.js-yxt-AccordionCard');
+      accordionBodyEl.style.height = `${this.isExpanded ? accordionBodyEl.scrollHeight : 0}px`;
+      DOM.on(toggleEl, 'click', () => this.handleClick(toggleEl, accordionBodyEl, accordionEl));
+    }
+  }
+
+  /**
+   * For passing functions to the config of children {@link CTACollectionComponent}
+   */
+  addChild (data, type, opts) {
+    if (type === CTACollectionComponent.type) {
+      const updatedData = {
+        verticalKey: this.verticalKey,
+        result: data
+      };
+      return super.addChild(updatedData, type, {
+        callsToAction: this._config.callsToAction,
+        _ctaModifiers: ['AccordionCard'],
+        isUniversal: this._config.isUniversal,
+        ...opts
+      });
+    }
+    return super.addChild(data, type, opts);
+  }
+
+  static get type () {
+    return cardTypes.Accordion;
+  }
+
+  static defaultTemplateName () {
+    return cardTemplates.Accordion;
+  }
+
+  static areDuplicateNamesAllowed () {
+    return true;
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/ui_components_cards_cardcomponent.js.html b/docs/ui_components_cards_cardcomponent.js.html new file mode 100644 index 000000000..ea27f5f77 --- /dev/null +++ b/docs/ui_components_cards_cardcomponent.js.html @@ -0,0 +1,229 @@ + + + + + JSDoc: Source: ui/components/cards/cardcomponent.js + + + + + + + + + + +
+ +

Source: ui/components/cards/cardcomponent.js

+ + + + + + +
+
+
/** @module CardComponent */
+
+import Component from '../component';
+import DOM from '../../dom/dom';
+import { cardTypes } from './consts';
+import AnalyticsEvent from '../../../core/analytics/analyticsevent';
+
+class CardConfig {
+  constructor (config = {}) {
+    Object.assign(this, config);
+
+    /**
+     * The card type to use
+     * @type {string}
+     */
+    this.cardType = config.cardType || 'Standard';
+
+    /**
+     * Data mappings is a function specified in the config
+     * that returns config based on the data passed into card
+     * @type {Function}
+     */
+    this.dataMappings = config.dataMappings || (() => {});
+
+    /**
+     * Either a function that spits out an array of CTA config objects or an array of CTA config objects
+     * or api fieldnames
+     * @type {Function|Array<Object|string>}
+     */
+    this.callsToAction = config.callsToAction || [];
+
+    /**
+     * The index of the card.
+     * @type {number}
+     */
+    this._index = config._index || 0;
+
+    /**
+     * Whether this card is part of a universal search
+     */
+    this.isUniversal = config.isUniversal || false;
+  }
+}
+
+export default class CardComponent extends Component {
+  constructor (config = {}, systemConfig = {}) {
+    super(new CardConfig(config), systemConfig);
+
+    /**
+     * config.data comes from the data-prop attribute passed in
+     * from the parent component.
+     * @type {Object}
+     */
+    const data = config.data || {};
+
+    /**
+     * The result data for this card.
+     * @type {Result}
+     */
+    this.result = data.result || {};
+
+    /**
+     * Vertical key for the search.
+     * @type {string}
+     */
+    this.verticalKey = data.verticalKey;
+  }
+
+  onMount () {
+    const rtfElement = DOM.query(this._container, '.js-yxt-rtfValue');
+    if (rtfElement) {
+      const fieldName = rtfElement.dataset.fieldName;
+      DOM.on(rtfElement, 'click', e => this._handleRtfClickAnalytics(e, fieldName));
+    }
+  }
+
+  /**
+   * A click handler for links in a Rich Text attriubte. When such a link is
+   * clicked, an {@link AnalyticsEvent} needs to be fired.
+   *
+   * @param {MouseEvent} event The click event.
+   * @param {string} fieldName The name of the Rich Text field used in the
+   *                           attriubte.
+   */
+  _handleRtfClickAnalytics (event, fieldName) {
+    const ctaType = event.target.dataset.ctaType;
+    if (!ctaType) {
+      return;
+    }
+
+    const analyticsOptions = {
+      directAnswer: false,
+      verticalKey: this._config.data.verticalKey,
+      searcher: this._config.isUniversal ? 'UNIVERSAL' : 'VERTICAL',
+      entityId: this._config.data.result.id,
+      url: event.target.href
+    };
+    if (!fieldName) {
+      console.warn('Field name not provided for RTF click analytics');
+    } else {
+      analyticsOptions.fieldName = fieldName;
+    }
+
+    const analyticsEvent = new AnalyticsEvent(ctaType);
+    analyticsEvent.addOptions(analyticsOptions);
+    this.analyticsReporter.report(analyticsEvent);
+  }
+
+  setState (data) {
+    const cardType = this._config.cardType;
+
+    // Use the cardType as component name if it is not a built-in type
+    const cardComponentName = cardTypes[cardType] || cardType;
+    return super.setState({
+      ...data,
+      result: this.result,
+      cardType: cardComponentName
+    });
+  }
+
+  addChild (data, type, opts) {
+    const updatedData = {
+      verticalKey: this.verticalKey,
+      result: data
+    };
+    const newOpts = {
+      showOrdinal: this._config.showOrdinal,
+      dataMappings: this._config.dataMappings,
+      callsToAction: this._config.callsToAction,
+      verticalKey: this._config.verticalKey,
+      _index: this._config._index,
+      isUniversal: this._config.isUniversal,
+      modifier: this._config.modifier,
+      ...opts
+    };
+    return super.addChild(updatedData, type, newOpts);
+  }
+
+  /**
+   * Used by children card components like StandardCardComponent to
+   * apply given template mappings as config.
+   * @param {Result} result
+   * @param {Object|Function} dataMappings
+   */
+  static applyDataMappings (result, dataMappings) {
+    const config = {};
+    if (typeof dataMappings === 'function') {
+      dataMappings = dataMappings(result);
+    }
+    if (typeof dataMappings === 'object') {
+      Object.entries(dataMappings).forEach(([attribute, value]) => {
+        if (typeof value === 'function') {
+          config[attribute] = value(result);
+        } else {
+          config[attribute] = value;
+        }
+      });
+    }
+    return config;
+  }
+
+  static get type () {
+    return 'Card';
+  }
+
+  /**
+   * The template to render
+   * @returns {string}
+   * @override
+   */
+  static defaultTemplateName (config) {
+    return 'cards/card';
+  }
+
+  static areDuplicateNamesAllowed () {
+    return true;
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/ui_components_cards_legacycardcomponent.js.html b/docs/ui_components_cards_legacycardcomponent.js.html new file mode 100644 index 000000000..6b349b3f8 --- /dev/null +++ b/docs/ui_components_cards_legacycardcomponent.js.html @@ -0,0 +1,232 @@ + + + + + JSDoc: Source: ui/components/cards/legacycardcomponent.js + + + + + + + + + + +
+ +

Source: ui/components/cards/legacycardcomponent.js

+ + + + + + +
+
+
/** @module LegacyCardComponent */
+
+import Component from '../component';
+import CardComponent from './cardcomponent';
+import { cardTemplates, cardTypes } from './consts';
+import CTACollectionComponent from '../ctas/ctacollectioncomponent';
+
+class LegacyCardConfig {
+  constructor (config = {}) {
+    Object.assign(this, config);
+
+    const data = config.data || {};
+
+    /**
+     * The result data
+     * @type {Result}
+     */
+    const result = data.result || {};
+
+    /**
+     * The raw profile data
+     * @type {Object}
+     */
+    const rawResult = result._raw || {};
+
+    /**
+     * The dataMappings attribute of the config
+     * is either a function that returns additional config for
+     * a card or an object that is the additional config.
+     */
+    Object.assign(this, CardComponent.applyDataMappings(rawResult, config.dataMappings || {}));
+
+    /**
+     * The result data
+     * @type {Result}
+     */
+    this.result = config.data || {};
+
+    /**
+     * Title for the card
+     * @type {string}
+     */
+    this.title = this.title || result.title || rawResult.name || '';
+
+    /**
+     * Details for the card
+     * @type {string}
+     */
+    this.details = this.details === null ? null : (this.details || result.details || rawResult.description || '');
+
+    /**
+     * Url when you click the title
+     * @type {string}
+     */
+    this.url = this.url === null ? '' : (this.url || result.link || rawResult.website);
+
+    /**
+     * The target attribute for the title link.
+     * @type {string}
+     */
+    this.target = this.target;
+
+    /**
+     * Image url to display
+     * @type {string}
+     */
+    this.image = this.image;
+
+    /**
+     * Subtitle
+     * @type {string}
+     */
+    this.subtitle = this.subtitle;
+
+    /**
+     * Either a function that spits out an array of CTA config objects or an array of CTA config objects
+     * or api fieldnames
+     * @type {Function|Array<Object|string>}
+     */
+    this.callsToAction = this.callsToAction || [];
+
+    /**
+     * Whether to show the ordinal of the card in the results.
+     * @type {boolean}
+     */
+    this.showOrdinal = this.showOrdinal || false;
+
+    /**
+     * Whether this card is part of a universal search.
+     * @type {boolean}
+     */
+    this.isUniversal = this.isUniversal || false;
+
+    /**
+     * The index of the card.
+     * @type {number}
+     */
+    this._index = config._index || 0;
+  }
+}
+
+/**
+ * Card components expect to receive a data config option, containing data regarding entity result
+ * each card is assigned to, including all field data in data._raw.
+ */
+export default class LegacyCardComponent extends Component {
+  constructor (config = {}, systemConfig = {}) {
+    super(new LegacyCardConfig(config), systemConfig);
+    /**
+     * @type {Object}
+     */
+    const data = config.data || {};
+
+    /**
+     * Vertical key for the search.
+     * @type {string}
+     */
+    this.verticalKey = data.verticalKey;
+
+    /**
+     * The result data
+     * @type {Result}
+     */
+    this.result = data.result || {};
+  }
+
+  setState (data) {
+    return super.setState({
+      ...data,
+      eventOptions: this._legacyEventOptions(this.result._raw.id, this.result.link),
+      result: this.result,
+      hasCTAs: CTACollectionComponent.hasCTAs(this.result._raw, this._config.callsToAction),
+      entityId: this.result._raw.id,
+      verticalKey: this.verticalKey
+    });
+  }
+
+  _legacyEventOptions (entityId, url) {
+    const options = {
+      verticalConfigId: this.verticalKey,
+      searcher: this._config.isUniversal ? 'UNIVERSAL' : 'VERTICAL'
+    };
+
+    if (entityId) {
+      options.entityId = entityId;
+    } else {
+      options.url = url;
+    }
+
+    return JSON.stringify(options);
+  }
+
+  addChild (data, type, opts) {
+    if (type === CTACollectionComponent.type) {
+      const updatedData = {
+        verticalKey: this.verticalKey,
+        result: data
+      };
+      return super.addChild(updatedData, type, {
+        callsToAction: this._config.callsToAction,
+        isUniversal: this._config.isUniversal,
+        _ctaModifiers: ['LegacyCard'],
+        includeLegacyClasses: true,
+        ...opts
+      });
+    }
+    return super.addChild(data, type, opts);
+  }
+
+  static get type () {
+    return cardTypes.Legacy;
+  }
+
+  static defaultTemplateName () {
+    return cardTemplates.Legacy;
+  }
+
+  static areDuplicateNamesAllowed () {
+    return true;
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/ui_components_cards_standardcardcomponent.js.html b/docs/ui_components_cards_standardcardcomponent.js.html new file mode 100644 index 000000000..893a48156 --- /dev/null +++ b/docs/ui_components_cards_standardcardcomponent.js.html @@ -0,0 +1,269 @@ + + + + + JSDoc: Source: ui/components/cards/standardcardcomponent.js + + + + + + + + + + +
+ +

Source: ui/components/cards/standardcardcomponent.js

+ + + + + + +
+
+
/** @module StandardCardComponent */
+
+import Component from '../component';
+import CardComponent from './cardcomponent';
+import { cardTemplates, cardTypes } from './consts';
+import DOM from '../../dom/dom';
+import CTACollectionComponent from '../ctas/ctacollectioncomponent';
+import TranslationFlagger from '../../i18n/translationflagger';
+
+class StandardCardConfig {
+  constructor (config = {}) {
+    Object.assign(this, config);
+
+    const data = config.data || {};
+
+    /**
+     * The result data
+     * @type {Result}
+     */
+    const result = data.result || {};
+
+    /**
+     * The raw profile data
+     * @type {Object}
+     */
+    const rawResult = result._raw || {};
+
+    /**
+     * The dataMappings attribute of the config
+     * is either a function that returns additional config for
+     * a card or an object that is the additional config.
+     */
+    Object.assign(this, CardComponent.applyDataMappings(rawResult, config.dataMappings || {}));
+
+    /**
+     * The result data
+     * @type {Result}
+     */
+    this.result = config.data || {};
+
+    /**
+     * Title for the card
+     * @type {string}
+     */
+    this.title = this.title || result.title || rawResult.name || '';
+
+    /**
+     * Details for the card
+     * @type {string}
+     */
+    this.details = this.details === null ? null : (this.details || result.details || rawResult.description || '');
+
+    /**
+     * Url when you click the title
+     * @type {string}
+     */
+    this.url = this.url === null ? '' : (this.url || result.link || rawResult.website);
+
+    /**
+     * If showMoreLimit is set, the text that displays beneath it
+     * @type {string}
+     */
+    this.showMoreText = this.showMoreText || TranslationFlagger.flag({
+      phrase: 'Show More',
+      context: 'Displays more details for the result'
+    });
+
+    /**
+     * If showMoreLimit is set, the text that displays beneath it when all text is shown
+     * @type {string}
+     */
+    this.showLessText = this.showLessText || TranslationFlagger.flag({
+      phrase: 'Show Less',
+      context: 'Displays less details for the result'
+    });
+
+    /**
+     * Add a show more link if this number of characters is shown,
+     * and truncate the last 3 characters with an ellipses.
+     * Clicking show more should expand the results (but no “show less” link).
+     * @type {number}
+     */
+    this.showMoreLimit = this.showMoreLimit;
+
+    /**
+     * The target attribute for the title link.
+     * @type {string}
+     */
+    this.target = this.target;
+
+    /**
+     * Image url to display
+     * @type {string}
+     */
+    this.image = this.image;
+
+    /**
+     * Subtitle
+     * @type {string}
+     */
+    this.subtitle = this.subtitle;
+
+    /**
+     * Whether a 'show more' toggle button needs to be rendered at all
+     */
+    const detailsOverLimit = this.details.length > this.showMoreLimit;
+    this.showToggle = this.showMoreLimit && detailsOverLimit;
+
+    /**
+     * Either a function that spits out an array of CTA config objects or an array of CTA config objects
+     * or api fieldnames
+     * @type {Function|Array<Object|string>}
+     */
+    this.callsToAction = this.callsToAction || [];
+
+    /**
+     * Whether to show the ordinal of the card in the results.
+     * @type {boolean}
+     */
+    this.showOrdinal = this.showOrdinal || false;
+
+    /**
+     * Whether this card is part of a universal search.
+     * @type {boolean}
+     */
+    this.isUniversal = this.isUniversal || false;
+
+    /**
+     * The index of the card.
+     * @type {number}
+     */
+    this._index = config._index || 0;
+  }
+}
+
+/**
+ * Card components expect to receive a data config option, containing data regarding entity result
+ * each card is assigned to, including all field data in data._raw.
+ */
+export default class StandardCardComponent extends Component {
+  constructor (config = {}, systemConfig = {}) {
+    super(new StandardCardConfig(config), systemConfig);
+    this.hideExcessDetails = this._config.showToggle;
+
+    /**
+     * @type {Object}
+     */
+    const data = config.data || {};
+
+    /**
+     * Vertical key for the search.
+     * @type {string}
+     */
+    this.verticalKey = data.verticalKey;
+
+    /**
+     * The result data
+     * @type {Result}
+     */
+    this.result = data.result || {};
+  }
+
+  setState (data) {
+    let details = this._config.details;
+    if (this._config.showMoreLimit) {
+      details = this.hideExcessDetails
+        ? `${this._config.details.substring(0, this._config.showMoreLimit)}...`
+        : this._config.details;
+    }
+    return super.setState({
+      ...data,
+      hideExcessDetails: this.hideExcessDetails,
+      result: this.result,
+      hasCTAs: CTACollectionComponent.hasCTAs(this.result._raw, this._config.callsToAction),
+      entityId: this.result._raw.id,
+      verticalKey: this.verticalKey,
+      details
+    });
+  }
+
+  onMount () {
+    if (this._config.showToggle) {
+      const el = DOM.query(this._container, '.js-yxt-StandardCard-toggle');
+      DOM.on(el, 'click', () => {
+        this.hideExcessDetails = !this.hideExcessDetails;
+        this.setState();
+      });
+    }
+  }
+
+  addChild (data, type, opts) {
+    if (type === CTACollectionComponent.type) {
+      const updatedData = {
+        verticalKey: this.verticalKey,
+        result: data
+      };
+      return super.addChild(updatedData, type, {
+        callsToAction: this._config.callsToAction,
+        isUniversal: this._config.isUniversal,
+        _ctaModifiers: ['StandardCard'],
+        ...opts
+      });
+    }
+    return super.addChild(data, type, opts);
+  }
+
+  static get type () {
+    return cardTypes.Standard;
+  }
+
+  static defaultTemplateName () {
+    return cardTemplates.Standard;
+  }
+
+  static areDuplicateNamesAllowed () {
+    return true;
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/ui_components_component.js.html b/docs/ui_components_component.js.html index 4261b7525..8a4b30fc1 100644 --- a/docs/ui_components_component.js.html +++ b/docs/ui_components_component.js.html @@ -28,12 +28,15 @@

Source: ui/components/component.js

/** @module Component */
 
+import cloneDeep from 'lodash.clonedeep';
+
 import { Renderers } from '../rendering/const';
 
 import DOM from '../dom/dom';
 import State from './state';
 import { AnalyticsReporter } from '../../core'; // eslint-disable-line no-unused-vars
 import AnalyticsEvent from '../../core/analytics/analyticsevent';
+import { AnswersComponentError } from '../../core/errors/errors';
 
 /**
  * Component is an abstraction that encapsulates state, behavior,
@@ -43,28 +46,26 @@ 

Source: ui/components/component.js

* mounted, created, etc. */ export default class Component { - constructor (type, opts = {}) { - // Simple facade pattern to enable the user to pass a single object - // containing all the necessary options and settings - if (typeof type === 'object') { - opts = type; - type = opts.type; - } - + constructor (config = {}, systemConfig = {}) { this.moduleId = null; /** - * Unique name of this component instance - * Used to distinguish between other components of the same type + * A unique id number for the component. + * @type {number} + */ + this.uniqueId = systemConfig.uniqueId; + + /** + * Name of this component instance. * @type {String} */ - this.name = opts.name || this.constructor.name; + this.name = config.name || this.constructor.type; /** * Cache the options so that we can propogate properly to child components * @type {Object} */ - this._opts = opts; + this._config = config; /** * An identifier used to classify the type of component. @@ -77,7 +78,7 @@

Source: ui/components/component.js

* A local reference to the parent component, if exists * @type {Component} */ - this._parent = opts.parent || null; + this._parentContainer = config.parentContainer || null; /** * A container for all the child components @@ -89,83 +90,95 @@

Source: ui/components/component.js

* The state (data) of the component to be provided to the template for rendering * @type {object} */ - this._state = new State(opts.state); + this._state = new State(config.state); /** * TODO(billy) This should be 'services' */ - this.core = opts.core || null; + this.core = systemConfig.core || null; /** * A local reference to the component manager, which contains all of the component classes * eligible to be created * @type {ComponentManager} */ - this.componentManager = opts.componentManager || null; + this.componentManager = systemConfig.componentManager || null; /** * A local reference to the analytics reporter, used to report events for this component * @type {AnalyticsReporter} */ - this.analyticsReporter = opts.analyticsReporter || null; + this.analyticsReporter = systemConfig.analyticsReporter || null; + + /** + * Options to include with all analytic events sent by this component + * @type {object} + * @private + */ + this._analyticsOptions = config.analyticsOptions || {}; + + /** + * Allows the main thread to regain control while rendering child components + * @type {boolean} + */ + this._progressivelyRenderChildren = config.progressivelyRenderChildren; /** * A reference to the DOM node that the component will be appended to when mounted/rendered. * @type {HTMLElement} */ - if (this._parent === null) { - if (typeof opts.container !== 'string') { - throw new Error('Missing `container` option for component configuration. Must be of type `string`.'); + if (this._parentContainer === null) { + if (typeof config.container === 'string') { + this._container = DOM.query(config.container) || null; + if (this._container === null) { + throw new Error('Cannot find container DOM node: ' + config.container); + } } - this._container = DOM.query(opts.container) || null; } else { - this._container = DOM.query(this._parent._container, opts.container); + this._container = DOM.query(this._parentContainer, config.container); // If we have a parent, and the container is missing from the DOM, // we construct the container and append it to the parent if (this._container === null) { this._container = DOM.createEl('div', { - class: opts.container.substring(1, opts.container.length) + class: config.container.substring(1, config.container.length) }); - DOM.append(this._parent._container, this._container); + DOM.append(this._parentContainer, this._container); } } - if (this._container === null) { - throw new Error('Cannot find container DOM node: ' + opts.container); - } - /** - * A custom class to be applied to {this._container} node + * A custom class to be applied to {this._container} node. Note that the class + * 'yxt-Answers-component' will be included as well. * @type {string} */ - this._className = opts.class || 'component'; + this._className = config.class || 'component'; /** * A custom render function to be used instead of using the default renderer * @type {Renderer} */ - this._render = opts.render || null; + this._render = config.render || null; /** * A local reference to the default {Renderer} that will be used for rendering the template * @type {Renderer} */ - this._renderer = opts.renderer || Renderers.Handlebars; + this._renderer = systemConfig.renderer || Renderers.Handlebars; /** * The template string to use for rendering the component * If this is left empty, we lookup the template the base templates using the templateName * @type {string} */ - this._template = opts.template ? this._renderer.compile(opts.template) : null; + this._template = config.template ? this._renderer.compile(config.template) : null; /** * The templateName to use for rendering the component. * This is only used if _template is empty. * @type {string} */ - this._templateName = opts.templateName || 'default'; + this._templateName = config.templateName || this.constructor.defaultTemplateName(config); /** * An internal state indicating whether or not the component has been mounted to the DOM @@ -179,25 +192,55 @@

Source: ui/components/component.js

* By default, no transformation happens. * @type {function} */ - this.transformData = opts.transformData || this.transformData || function () {}; + this.transformData = config.transformData; /** * The a local reference to the callback that will be invoked when a component is created. * @type {function} */ - this.onCreate = opts.onCreate || this.onCreate || function () {}; + this.onCreate = config.onCreateOverride || this.onCreate || function () {}; + this.onCreate = this.onCreate.bind(this); /** * The a local reference to the callback that will be invoked when a component is Mounted. * @type {function} */ - this.onMount = opts.onMount || this.onMount || function () { }; + this.onMount = config.onMountOverride || this.onMount || function () {}; + this.onMount = this.onMount.bind(this); /** * The a local reference to the callback that will be invoked when a components state is updated. * @type {function} */ - this.onUpdate = opts.onUpdate || this.onUpdate || function () { }; + this.onUpdate = config.onUpdateOverride || this.onUpdate || function () { }; + this.onUpdate = this.onUpdate.bind(this); + + /** + * A user provided onCreate callback + * @type {function} + */ + this.userOnCreate = config.onCreate || function () {}; + + /** + * A user provided onMount callback + * @type {function} + */ + this.userOnMount = config.onMount || function () {}; + + /** + * A user provided onUpdate callback + * @type {function} + */ + this.userOnUpdate = config.onUpdate || function () {}; + } + + /** + * The template to render + * @returns {string} + * @override + */ + static defaultTemplateName (config) { + return 'default'; } static get type () { @@ -209,19 +252,55 @@

Source: ui/components/component.js

} init (opts) { - this.setState(opts.data || opts.state || {}); - this.onCreate(); + try { + this.setState(opts.data || opts.state || {}); + this.onCreate(); + this.userOnCreate(); + } catch (e) { + throw new AnswersComponentError( + 'Error initializing component', + this.constructor.type, + e); + } + this._state.on('update', () => { - this.onUpdate(); - this.mount(); + try { + this.onUpdate(); + this.userOnUpdate(); + this.unMount(); + this.mount(); + } catch (e) { + throw new AnswersComponentError( + 'Error updating component', + this.constructor.type, + e); + } }); DOM.addClass(this._container, this._className); + DOM.addClass(this._container, 'yxt-Answers-component'); return this; } + /** + * Adds a class to the container of the component. + * @param {string} className A comma separated value of classes + */ + addContainerClass (className) { + DOM.addClass(this._container, className); + } + + /** + * Removes the specified classes from the container of the component + * @param {string} className A comma separated value of classes + */ + removeContainerClass (className) { + DOM.removeClass(this._container, className); + } + setState (data) { - this._state.set(data); + const newState = Object.assign({}, { _config: this._config }, data); + this._state.set(newState); return this; } @@ -233,19 +312,15 @@

Source: ui/components/component.js

return this._state.has(prop); } - transformData (data) { - return data; - } - addChild (data, type, opts) { let childComponent = this.componentManager.create( type, Object.assign({ name: data.name, - parent: this, + parentContainer: this._container, data: data }, opts || {}, { - _parentOpts: this._opts + _parentOpts: this._config }) ); @@ -253,6 +328,15 @@

Source: ui/components/component.js

return childComponent; } + /** + * Unmount and remove this component and its children from the list + * of active components + */ + remove () { + this._children.forEach(c => c.remove()); + this.componentManager.remove(this); + } + /** * Set the render method to be used for rendering the component * @param {Function} render @@ -281,49 +365,80 @@

Source: ui/components/component.js

} unMount () { + if (!this._container) { + return this; + } + + this._children.forEach(child => { + child.unMount(); + }); + DOM.empty(this._container); + this._children.forEach(c => c.remove()); + this._children = []; + this.onUnMount(); } - mount () { + mount (container) { + if (container) { + this._container = container; + } + if (!this._container) { return this; } - this.unMount(); if (this.beforeMount() === false) { return this; } DOM.append(this._container, this.render(this._state.asJSON())); - this._isMounted = true; - this._onMount(); + // Process the DOM to determine if we should create + // in-memory sub-components for rendering + const domComponents = DOM.queryAll(this._container, '[data-component]:not([data-is-component-mounted])'); + const data = this.transformData + ? this.transformData(cloneDeep(this._state.get())) + : this._state.get(); + domComponents.forEach(c => this._createSubcomponent(c, data)); - // Attach analytics hooks as necessary - let domHooks = DOM.queryAll(this._container, '[data-eventtype]'); - domHooks.forEach(this._createAnalyticsHook.bind(this)); + if (this._progressivelyRenderChildren) { + this._children.forEach(child => { + setTimeout(() => { + child.mount(); + }); + }); + } else { + this._children.forEach(child => { + child.mount(); + }); + } - return this; - } + // Attach analytics hooks as necessary + if (this.analyticsReporter) { + let domHooks = DOM.queryAll(this._container, '[data-eventtype]:not([data-is-analytics-attached])'); + domHooks.forEach(this._createAnalyticsHook.bind(this)); + } - _onMount () { + this._isMounted = true; this.onMount(this); - if (this._children.length === 0) { - return; - } + this.userOnMount(this); - this._children.forEach(child => { - child._onMount(); - }); + DOM.removeClass(this._container, 'yxt-Answers-component--unmounted'); + + return this; } /** * render the template using the {Renderer} with the current state and template of the component * @returns {string} */ - render (data) { + render (data = this._state.get()) { this.beforeRender(); - data = this.transformData(data) || this.transformData(this._state.get()); + // Temporary fix for passing immutable data to transformData(). + data = this.transformData + ? this.transformData(cloneDeep(data)) + : data; let html = ''; // Use either the custom render function or the internal renderer @@ -345,28 +460,23 @@

Source: ui/components/component.js

// So that we can query it for processing of sub components let el = DOM.create(html); - // Process the DOM to determine if we should create - // in-memory sub-components for rendering - let domComponents = DOM.queryAll(el, '[data-component]'); - domComponents.forEach(c => this._createSubcomponent(c, data)); - this.afterRender(); return el.innerHTML; } _createSubcomponent (domComponent, data) { - let dataset = domComponent.dataset; - let type = dataset.component; - let prop = dataset.prop; + domComponent.dataset.isComponentMounted = true; + const dataset = domComponent.dataset; + const type = dataset.component; + const prop = dataset.prop; let opts = dataset.opts ? JSON.parse(dataset.opts) : {}; - // Rendering a sub component should be within the context, - // of the node that we processed it from - opts = Object.assign(opts, { - container: domComponent - }); + let childData = data[prop] || {}; - let childData = data[prop]; + opts = { + ...opts, + container: domComponent + }; // TODO(billy) Right now, if we provide an array as the data prop, // the behavior is to create many components for each item in the array. @@ -375,31 +485,33 @@

Source: ui/components/component.js

// to create many components for each item. // Overloading and having this side effect is unintuitive and WRONG if (!Array.isArray(childData)) { - let childComponent = this.addChild(childData, type, opts); - DOM.append(domComponent, childComponent.render()); + // Rendering a sub component should be within the context, + // of the node that we processed it from + this.addChild(childData, type, opts); return; } - // Otherwise, render the component as expected - let childHTML = []; - for (let i = 0; i < childData.length; i++) { - let childComponent = this.addChild(childData[i], type, opts); - childHTML.push(childComponent.render()); - } - - DOM.append(domComponent, childHTML.join('')); + childData.reverse(); + childData.forEach(data => { + this.addChild(data, type, opts); + }); } _createAnalyticsHook (domComponent) { + domComponent.dataset.isAnalyticsAttached = true; const dataset = domComponent.dataset; const type = dataset.eventtype; const label = dataset.eventlabel; + const middleclick = dataset.middleclick; const options = dataset.eventoptions ? JSON.parse(dataset.eventoptions) : {}; - DOM.on(domComponent, 'mousedown', () => { - const event = new AnalyticsEvent(type, label); - event.addOptions(options); - this.analyticsReporter.report(event); + DOM.on(domComponent, 'mousedown', e => { + if (e.button === 0 || (middleclick && e.button === 1)) { + const event = new AnalyticsEvent(type, label); + event.addOptions(this._analyticsOptions); + event.addOptions(options); + this.analyticsReporter.report(event); + } }); } @@ -477,13 +589,13 @@

Source: ui/components/component.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/ui_components_componentmanager.js.html b/docs/ui_components_componentmanager.js.html index a70f62277..9b9211f58 100644 --- a/docs/ui_components_componentmanager.js.html +++ b/docs/ui_components_componentmanager.js.html @@ -29,6 +29,10 @@

Source: ui/components/componentmanager.js

/** @module ComponentManager */
 
 import { AnswersComponentError } from '../../core/errors/errors';
+import DOM from '../dom/dom';
+import { COMPONENT_REGISTRY } from './registry';
+
+/** @typedef {import('../../core/core').default} Core */
 
 /**
  * ComponentManager is a Singletone that contains both an internal registry of
@@ -39,24 +43,17 @@ 

Source: ui/components/componentmanager.js

*/ export default class ComponentManager { constructor () { - if (!ComponentManager.setInstance(this)) { - return ComponentManager.getInstance(); - } - - /** - * The component registry is an internal map, that contains - * all available component CLASSES used for creation or override. - * Each component class has a unique TYPE, which is used as the key for the registry - * @type {Object} - */ - this._componentRegistry = {}; - /** * The active components is an internal container to keep track * of all of the components that have been constructed */ this._activeComponents = []; + /** + * A counter for the id the give to the next component that is created. + */ + this._componentIdCounter = 0; + /** * A local reference to the core library dependency * @@ -79,17 +76,23 @@

Source: ui/components/componentmanager.js

* A local reference to the analytics reporter dependency */ this._analyticsReporter = null; + + /** + * A mapping from component types to component names, as these may be configured by a user + */ + this._componentTypeToComponentNames = {}; + + /** + * A mapping of Components to moduleId storage listeners, for removal purposes. + */ + this._componentToModuleIdListener = new Map(); } - static setInstance (instance) { + static getInstance () { if (!this.instance) { - this.instance = instance; - return true; + this.instance = new ComponentManager(); } - return false; - } - static getInstance () { return this.instance; } @@ -113,10 +116,24 @@

Source: ui/components/componentmanager.js

* @param {Component} The Component Class to register */ register (componentClazz) { - this._componentRegistry[componentClazz.type] = componentClazz; + COMPONENT_REGISTRY[componentClazz.type] = componentClazz; return this; } + /** + * Returns components with names similar to the passed in component class. + * @param {string} componentType + */ + getSimilarComponents (componentType) { + let similarComponents = Object.keys(COMPONENT_REGISTRY).filter(type => + type.startsWith(componentType.substring(0, 2)) + ); + if (similarComponents.length === 0) { + similarComponents = Object.keys(COMPONENT_REGISTRY); + } + return similarComponents; + } + /** * create is the entry point for constructing any and all components. * It will instantiate a new instance of the component, and both apply @@ -128,14 +145,21 @@

Source: ui/components/componentmanager.js

// Every component needs local access to the component manager // because sometimes components have subcomponents that need to be // constructed during creation - opts = Object.assign({ + let systemOpts = { core: this._core, renderer: this._renderer, analyticsReporter: this._analyticsReporter, - componentManager: this - }, opts); + componentManager: this, + uniqueId: this._componentIdCounter + }; + this._componentIdCounter++; - let componentClass = this._componentRegistry[componentType]; + let componentClass = COMPONENT_REGISTRY[componentType]; + if (!componentClass) { + throw new AnswersComponentError( + `Component type ${componentType} is not recognized as a valid component.` + + ` You might have meant ${this.getSimilarComponents(componentType).join(', ')}?`); + } if ( !componentClass.areDuplicateNamesAllowed() && @@ -146,32 +170,78 @@

Source: ui/components/componentmanager.js

componentType); } + const config = { + isTwin: this._activeComponents.some(component => component.constructor.type === componentType), + ...opts + }; + // Instantiate our new component and keep track of it let component = - new this._componentRegistry[componentType](opts) - .init(opts); + new COMPONENT_REGISTRY[componentType](config, systemOpts) + .init(config); this._activeComponents.push(component); + if (!this._componentTypeToComponentNames[componentType]) { + this._componentTypeToComponentNames[componentType] = []; + } + this._componentTypeToComponentNames[componentType].push(component.name); - // If there is a local storage to power state, apply the state + // If there is a storage to power state, apply the state // from the storage to the component, and then bind the component // state to the storage via its updates if (this._core && this._core.storage !== null) { if (component.moduleId === undefined || component.moduleId === null) { return component; } - - this._core.storage - .on('update', component.moduleId, (data) => { - component.setState(data); - }); + const listener = { + eventType: 'update', + storageKey: component.moduleId, + callback: data => component.setState(data) + }; + this._core.storage.registerListener(listener); + this._componentToModuleIdListener.set(component, listener); } return component; } + /** + * Remove the provided component from the list of active components and remove + * the associated storage event listener + * @param {Component} component The component to remove + */ + remove (component) { + this._core.storage.removeListener(this._componentToModuleIdListener.get(component)); + + const index = this._activeComponents.findIndex(c => c.uniqueId === component.uniqueId); + if (index !== -1) { + this._activeComponents.splice(index, 1); + } + } + + /** + * Remove the component with the given name + * @param {string} name The name of the compnent to remove + */ + removeByName (name) { + const component = this._activeComponents.find(c => c.name === name); + component.remove(); + DOM.empty(component._container); + } + getActiveComponent (type) { - return this._activeComponents.find(c => c.type === type); + return this._activeComponents.find(c => c.constructor.type === type); + } + + /** + * Returns a concatenated list of all names associated with the given component types + * @param {string[]} type The types of the component + * @returns {string[]} The component names for the component types + */ + getComponentNamesForComponentTypes (types) { + return types.reduce((names, type) => { + return names.concat(this._componentTypeToComponentNames[type] || []); + }, []); } }
@@ -184,13 +254,13 @@

Source: ui/components/componentmanager.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/ui_components_componenttypes.js.html b/docs/ui_components_componenttypes.js.html new file mode 100644 index 000000000..eb12fbceb --- /dev/null +++ b/docs/ui_components_componenttypes.js.html @@ -0,0 +1,68 @@ + + + + + JSDoc: Source: ui/components/componenttypes.js + + + + + + + + + + +
+ +

Source: ui/components/componenttypes.js

+ + + + + + +
+
+
/** @module */
+
+/**
+ * An enum listing the different Component types supported in the SDK
+ * TODO: add all component types
+ * @type {Object.<string, string>}
+ */
+export default {
+  FILTER_BOX: 'FilterBox',
+  FILTER_OPTIONS: 'FilterOptions',
+  RANGE_FILTER: 'RangeFilter',
+  DATE_RANGE_FILTER: 'DateRangeFilter',
+  FACETS: 'Facets',
+  GEOLOCATION_FILTER: 'GeoLocationFilter',
+  SORT_OPTIONS: 'SortOptions',
+  FILTER_SEARCH: 'FilterSearch'
+};
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/ui_components_const.js.html b/docs/ui_components_const.js.html deleted file mode 100644 index 52ca6b28f..000000000 --- a/docs/ui_components_const.js.html +++ /dev/null @@ -1,108 +0,0 @@ - - - - - JSDoc: Source: ui/components/const.js - - - - - - - - - - -
- -

Source: ui/components/const.js

- - - - - - -
-
-
/** @module */
-
-import ComponentManager from './componentmanager';
-
-import Component from './component';
-
-import NavigationComponent from './navigation/navigationcomponent';
-
-import SearchComponent from './search/searchcomponent';
-import FilterSearchComponent from './search/filtersearchcomponent';
-import AutoCompleteComponent from './search/autocompletecomponent';
-
-import FilterOptionsComponent from './filters/filteroptionscomponent';
-import FilterBoxComponent from './filters/filterboxcomponent';
-
-import DirectAnswerComponent from './results/directanswercomponent';
-import ResultsComponent from './results/resultscomponent';
-import UniversalResultsComponent from './results/universalresultscomponent';
-
-import ResultsItemComponent from './results/resultsitemcomponent';
-import LocationResultsItemComponent from './results/locationresultsitemcomponent';
-import EventResultsItemComponent from './results/eventresultsitemcomponent';
-import PeopleResultsItemComponent from './results/peopleresultsitemcomponent';
-
-import MapComponent from './map/mapcomponent';
-
-import QuestionSubmissionComponent from './questions/questionsubmissioncomponent';
-
-export const COMPONENT_MANAGER = new ComponentManager()
-// Core Component
-  .register(Component)
-
-// Navigation Components
-  .register(NavigationComponent)
-
-// Search Components
-  .register(SearchComponent)
-  .register(FilterSearchComponent)
-  .register(AutoCompleteComponent)
-
-// Filter Components
-  .register(FilterBoxComponent)
-  .register(FilterOptionsComponent)
-
-// Results Components
-  .register(DirectAnswerComponent)
-  .register(UniversalResultsComponent)
-  .register(ResultsComponent)
-  .register(ResultsItemComponent)
-  .register(LocationResultsItemComponent)
-  .register(EventResultsItemComponent)
-  .register(PeopleResultsItemComponent)
-
-  .register(MapComponent)
-
-// Questions Components
-  .register(QuestionSubmissionComponent);
-
-
-
- - - - -
- - - -
- -
- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) -
- - - - - diff --git a/docs/ui_components_ctas_ctacollectioncomponent.js.html b/docs/ui_components_ctas_ctacollectioncomponent.js.html new file mode 100644 index 000000000..a0812fa57 --- /dev/null +++ b/docs/ui_components_ctas_ctacollectioncomponent.js.html @@ -0,0 +1,183 @@ + + + + + JSDoc: Source: ui/components/ctas/ctacollectioncomponent.js + + + + + + + + + + +
+ +

Source: ui/components/ctas/ctacollectioncomponent.js

+ + + + + + +
+
+
/** @module CTACollectionComponent */
+
+import Component from '../component';
+
+export default class CTACollectionComponent extends Component {
+  constructor (config = {}, systemConfig = {}) {
+    super(config, systemConfig);
+
+    const data = this._config.data || {};
+
+    /**
+     * Result data
+     * @type {Result}
+     */
+    this.result = data.result || {};
+
+    /**
+     * Whether the DOM should include legacy class names
+     * @type {boolean}
+     */
+    this.includeLegacyClasses = this._config.includeLegacyClasses || false;
+
+    /**
+     * Vertical key for the search.
+     * @type {string}
+     */
+    this.verticalKey = data.verticalKey;
+
+    /**
+     * Whether this cta is part of a universal search.
+     * @type {boolean}
+     */
+    this.isUniversal = this._config.isUniversal || false;
+
+    /**
+     * Either a function that spits out an array of CTA config objects or an array of CTA config objects
+     * or api fieldnames
+     * @type {Function|Array<Object|string>}
+     */
+    const callsToAction = this._config.callsToAction || [];
+
+    /**
+     * The config for each calls to action component to render.
+     * @type {Array<Object>}
+     */
+    this.callsToAction = CTACollectionComponent.resolveCTAMapping(this.result._raw, ...callsToAction);
+
+    // Assign any extra cta config that does not come from the cta mappings.
+    const _ctaModifiers = this._config._ctaModifiers || [];
+    if (this.callsToAction.length === 1) {
+      _ctaModifiers.push('solo');
+    }
+    this.callsToAction = this.callsToAction.map(cta => ({
+      eventOptions: this.defaultEventOptions(this.result),
+      _ctaModifiers: _ctaModifiers,
+      includeLegacyClasses: this.includeLegacyClasses,
+      ...cta
+    }));
+  }
+
+  /**
+   * Handles resolving ctas from a cta mapping which are either
+   * 1. a function that returns a cta's config
+   * 2. an object that has a per-attribute mapping of either a
+   *    a) static value
+   *    b) function that takes in resut data and returns the given attributes value
+   * Note: Intentionally does not allow nesting functions.
+   * @param {Object} result
+   * @param {Function|...(Object|string)} ctas
+   * @returns {Array<Object>}
+   */
+  static resolveCTAMapping (result, ...ctas) {
+    let parsedCTAs = [];
+    ctas.map(ctaMapping => {
+      if (typeof ctaMapping === 'function') {
+        parsedCTAs = parsedCTAs.concat(ctaMapping(result));
+      } else if (typeof ctaMapping === 'object') {
+        const ctaObject = { ...ctaMapping };
+        for (let [ctaAttribute, attributeMapping] of Object.entries(ctaMapping)) {
+          if (typeof attributeMapping === 'function') {
+            ctaObject[ctaAttribute] = attributeMapping(result);
+          }
+        }
+        parsedCTAs.push(ctaObject);
+      }
+    });
+    parsedCTAs = parsedCTAs.filter(cta => cta);
+
+    parsedCTAs.forEach(cta => {
+      if (!cta.label && !cta.url) {
+        console.warn('Call to Action:', cta, 'is missing both a label and url attribute and is being automatically hidden');
+      } else if (!cta.label) {
+        console.warn('Call to Action:', cta, 'is missing a label attribute and is being automatically hidden');
+      } else if (!cta.url) {
+        console.warn('Call to Action:', cta, 'is missing a url attribute and is being automatically hidden');
+      }
+    });
+
+    return parsedCTAs.filter(cta => cta.url && cta.url.trim() && cta.label && cta.label.trim());
+  }
+
+  static hasCTAs (result, ctas) {
+    return CTACollectionComponent.resolveCTAMapping(result, ...ctas).length > 0;
+  }
+
+  defaultEventOptions (result) {
+    const eventOptions = {
+      verticalKey: this.verticalKey,
+      searcher: this._config.isUniversal ? 'UNIVERSAL' : 'VERTICAL'
+    };
+    if (result._raw.id) {
+      eventOptions.entityId = result._raw.id;
+    }
+    return eventOptions;
+  }
+
+  setState (data) {
+    return super.setState({
+      ...data,
+      includeLegacyClasses: this.includeLegacyClasses,
+      callsToAction: this.callsToAction
+    });
+  }
+
+  static get type () {
+    return 'CTACollection';
+  }
+
+  static defaultTemplateName () {
+    return 'ctas/ctacollection';
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/ui_components_ctas_ctacomponent.js.html b/docs/ui_components_ctas_ctacomponent.js.html new file mode 100644 index 000000000..b6cd597b1 --- /dev/null +++ b/docs/ui_components_ctas_ctacomponent.js.html @@ -0,0 +1,164 @@ + + + + + JSDoc: Source: ui/components/ctas/ctacomponent.js + + + + + + + + + + +
+ +

Source: ui/components/ctas/ctacomponent.js

+ + + + + + +
+
+
/** @module CTAComponent */
+
+import Component from '../component';
+import AnalyticsEvent from '../../../core/analytics/analyticsevent';
+import DOM from '../../dom/dom';
+
+class CTAConfig {
+  constructor (config = {}) {
+    Object.assign(this, config);
+
+    /**
+     * Label below the CTA icon
+     * @type {string}
+     */
+    this.label = config.label;
+
+    /**
+     * CTA icon, maps to a set of icons.
+     * @type {string}
+     */
+    this.icon = config.icon;
+
+    /**
+     * Url to custom icon, has priority over icon.
+     * @type {string}
+     */
+    this.iconUrl = config.iconUrl;
+
+    /**
+     * Whether the DOM should include legacy class names
+     * @type {boolean}
+     */
+    this.includeLegacyClasses = config.includeLegacyClasses || false;
+
+    /**
+     * Click through url for the icon and label
+     * @type {string}
+     */
+    this.url = config.url;
+
+    /**
+     * Analytics event that should fire:
+     * @type {string}
+     */
+    this.analyticsEventType = config.analytics || config.eventType || 'CTA_CLICK';
+
+    /**
+     * The target attribute for the CTA link.
+     * @type {boolean}
+     */
+    this.target = config.target || '_blank';
+
+    /**
+     * The eventOptions needed for the event to fire, passed as a string or Object
+     * from config.dataMappings || {}.
+     * @type {Object}
+     */
+    if (typeof config.eventOptions === 'string') {
+      this.eventOptions = JSON.parse(config.eventOptions);
+    }
+    this.eventOptions = this.eventOptions;
+
+    /**
+     * Additional css className modifiers for the cta
+     * @type {string}
+     */
+    this._ctaModifiers = config._ctaModifiers;
+
+    /**
+     * Whether the cta is the only one in its CTACollectionComponent
+     * @type {boolean}
+     */
+    this._isSolo = config._isSolo || false;
+  }
+}
+
+export default class CTAComponent extends Component {
+  constructor (config = {}, systemConfig = {}) {
+    super(new CTAConfig(config), systemConfig);
+  }
+
+  onMount () {
+    const el = DOM.query(this._container, `.js-yxt-CTA`);
+    if (el && this._config.eventOptions) {
+      DOM.on(el, 'mousedown', e => {
+        if (e.button === 0 || e.button === 1) {
+          this.reportAnalyticsEvent();
+        }
+      });
+    }
+  }
+
+  setState (data) {
+    return super.setState({
+      ...data,
+      hasIcon: this._config.icon || this._config.iconUrl
+    });
+  }
+
+  reportAnalyticsEvent () {
+    const analyticsEvent = new AnalyticsEvent(this._config.analyticsEventType);
+    analyticsEvent.addOptions(this._config.eventOptions);
+    this.analyticsReporter.report(analyticsEvent);
+  }
+
+  static get type () {
+    return 'CTA';
+  }
+
+  static defaultTemplateName (config) {
+    return 'ctas/cta';
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/ui_components_filters_daterangefiltercomponent.js.html b/docs/ui_components_filters_daterangefiltercomponent.js.html new file mode 100644 index 000000000..05f3c1946 --- /dev/null +++ b/docs/ui_components_filters_daterangefiltercomponent.js.html @@ -0,0 +1,326 @@ + + + + + JSDoc: Source: ui/components/filters/daterangefiltercomponent.js + + + + + + + + + + +
+ +

Source: ui/components/filters/daterangefiltercomponent.js

+ + + + + + +
+
+
/** @module DateFilterComponent */
+
+import DOM from '../../dom/dom';
+import Component from '../component';
+import ComponentTypes from '../../components/componenttypes';
+import TranslationFlagger from '../../i18n/translationflagger';
+import Filter from '../../../core/models/filter';
+import FilterNodeFactory from '../../../core/filters/filternodefactory';
+import FilterMetadata from '../../../core/filters/filtermetadata';
+import Matcher from '../../../core/filters/matcher';
+import StorageKeys from '../../../core/storage/storagekeys';
+import { getPersistedRangeFilterContents } from '../../tools/filterutils';
+
+/**
+ * A filter for a range of dates
+ */
+export default class DateRangeFilterComponent extends Component {
+  constructor (config = {}, systemConfig = {}) {
+    super(config, systemConfig);
+
+    /**
+     * The api field this filter controls
+     * @type {string}
+     * @private
+     */
+    this._field = config.field;
+
+    /**
+     * The title to display for the date range
+     * @type {string}
+     * @private
+     */
+    this._title = config.title;
+
+    /**
+     * The optional label to show for the min date input
+     * @type {string}
+     * @private
+     */
+    this._minLabel = config.minLabel || null;
+
+    /**
+     * The optional label to show for the max date input
+     * @type {string}
+     * @private
+     */
+    this._maxLabel = config.maxLabel || null;
+
+    /**
+     * The callback used when a date is changed
+     * @type {function}
+     * @private
+     */
+    this._onChange = config.onChange || function () {};
+
+    /**
+     * If true, stores the filter to storage on each change
+     * @type {boolean}
+     * @private
+     */
+    this._storeOnChange = config.storeOnChange === undefined ? true : config.storeOnChange;
+
+    /**
+     * If true, this filter represents an exclusive range, rather than an inclusive one
+     * @type {boolean}
+     * @private
+     */
+    this._isExclusive = config.isExclusive;
+
+    this.seedFromPersistedFilter();
+
+    this.core.storage.registerListener({
+      storageKey: StorageKeys.HISTORY_POP_STATE,
+      eventType: 'update',
+      callback: () => {
+        this.seedFromPersistedFilter();
+        this.setState();
+      }
+    });
+  }
+
+  /**
+   * Reseeds the component state from the PERSISTED_FILTER in storage.
+   * If there is an active filter, store it in core.
+   */
+  seedFromPersistedFilter () {
+    if (this.core.storage.has(StorageKeys.PERSISTED_FILTER)) {
+      const persistedFilter = this.core.storage.get(StorageKeys.PERSISTED_FILTER);
+      const persistedFilterContents = getPersistedRangeFilterContents(persistedFilter, this._field);
+      let minVal, maxVal;
+      if (this._isExclusive) {
+        minVal = persistedFilterContents[Matcher.GreaterThan];
+        maxVal = persistedFilterContents[Matcher.LessThan];
+      } else {
+        minVal = persistedFilterContents[Matcher.GreaterThanOrEqualTo];
+        maxVal = persistedFilterContents[Matcher.LessThanOrEqualTo];
+      }
+      this._date = {
+        min: minVal,
+        max: maxVal
+      };
+    } else {
+      const today = new Date();
+      const todayString = `${today.getFullYear()}-${`${today.getMonth() + 1}`.padStart(2, '0')}-${`${today.getDate()}`.padStart(2, '0')}`;
+
+      this._date = {
+        min: [this._config.initialMin, todayString].find(v => v !== undefined),
+        max: [this._config.initialMax, todayString].find(v => v !== undefined)
+      };
+    }
+
+    if (this._date.min != null || this._date.max != null) {
+      const filterNode = this.getFilterNode();
+      this.core.setStaticFilterNodes(this.name, filterNode);
+    }
+  }
+
+  static defaultTemplateName () {
+    return 'controls/date';
+  }
+
+  static get type () {
+    return ComponentTypes.DATE_RANGE_FILTER;
+  }
+
+  setState (data) {
+    super.setState(Object.assign({}, data, {
+      name: this.name,
+      title: this._title,
+      minLabel: this._minLabel,
+      maxLabel: this._maxLabel,
+      dateMin: this._date.min,
+      dateMax: this._date.max
+    }));
+  }
+
+  onCreate () {
+    DOM.delegate(this._container, '.js-yext-date', 'change', (event) => {
+      this._updateRange(event.target.dataset.key, event.target.value);
+    });
+  }
+
+  /**
+   * Set the min date to the one provided
+   * @param {string} date Date to set in yyyy-mm-dd string format
+   */
+  setMin (date) {
+    this._updateRange('min', date);
+  }
+
+  /**
+   * Set the max date to the one provided
+   * @param {string} date Date to set in yyyy-mm-dd string format
+   */
+  setMax (date) {
+    this._updateRange('max', date);
+  }
+
+  _removeFilterNode () {
+    this._date = {
+      min: null,
+      max: null
+    };
+    this.setState();
+    this._onChange(FilterNodeFactory.from());
+    this.core.clearStaticFilterNode(this.name);
+    this.core.storage.delete(`${this.name}.min`);
+    this.core.storage.delete(`${this.name}.max`);
+  }
+
+  /**
+   * Returns this component's filter node.
+   * This method is exposed so that components like {@link FilterBoxComponent}
+   * can access them.
+   * @returns {FilterNode}
+   */
+  getFilterNode () {
+    return FilterNodeFactory.from({
+      filter: this._buildFilter(),
+      metadata: this._buildFilterMetadata(),
+      remove: () => this._removeFilterNode()
+    });
+  }
+
+  /**
+   * Updates the current state of the date range
+   * @param {string} key The key for the date value
+   * @param {string} value The string date value
+   * @private
+   */
+  _updateRange (key, value) {
+    this._date = Object.assign({}, this._date, { [key]: value });
+    this.setState();
+
+    const filterNode = this.getFilterNode();
+    if (this._storeOnChange) {
+      this.core.setStaticFilterNodes(this.name, filterNode);
+    }
+    this.core.storage.setWithPersist(`${this.name}.min`, this._date.min);
+    this.core.storage.setWithPersist(`${this.name}.max`, this._date.max);
+
+    this._onChange(filterNode);
+  }
+
+  /**
+   * Construct an api filter with the current date state
+   * @private
+   */
+  _buildFilter () {
+    return Filter.range(this._field, this._date.min, this._date.max, this._isExclusive);
+  }
+
+  /**
+   * Helper method for creating a date range filter metadata
+   * @returns {FilterMetadata}
+   */
+  _buildFilterMetadata () {
+    const { min, max } = this._date;
+
+    if (!min && !max) {
+      return new FilterMetadata({
+        fieldName: this._title
+      });
+    }
+    let displayValue;
+    if (!max) {
+      displayValue = this._isExclusive
+        ? TranslationFlagger.flag({
+          phrase: 'After [[date]]',
+          context: 'After a date. Example: After [August 15th]',
+          interpolationValues: {
+            date: min
+          }
+        })
+        : TranslationFlagger.flag({
+          phrase: '[[date]] or later',
+          context: 'Beginning at a date (with no end date). Example: [August 15th] or later',
+          interpolationValues: {
+            date: min
+          }
+        });
+    } else if (!min) {
+      displayValue = this._isExclusive
+        ? TranslationFlagger.flag({
+          phrase: 'Before [[date]]',
+          context: 'Before a date. Example: Before [August 15th]',
+          interpolationValues: {
+            date: max
+          }
+        })
+        : TranslationFlagger.flag({
+          phrase: '[[date]] and earlier',
+          context: 'Ending at a date with (no start date). Example: [August 15th] or earlier',
+          interpolationValues: {
+            date: max
+          }
+        });
+    } else if (min === max) {
+      displayValue = this._isExclusive ? '' : min;
+    } else {
+      displayValue = TranslationFlagger.flag({
+        phrase: '[[start]] - [[end]]',
+        context: 'Start date to end date. Example: [August 15th] - [August 16th]',
+        interpolationValues: {
+          start: min,
+          end: max
+        }
+      });
+    }
+    return new FilterMetadata({
+      fieldName: this._title,
+      displayValue: displayValue
+    });
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/ui_components_filters_facetscomponent.js.html b/docs/ui_components_filters_facetscomponent.js.html new file mode 100644 index 000000000..a170b7ecf --- /dev/null +++ b/docs/ui_components_filters_facetscomponent.js.html @@ -0,0 +1,422 @@ + + + + + JSDoc: Source: ui/components/filters/facetscomponent.js + + + + + + + + + + +
+ +

Source: ui/components/filters/facetscomponent.js

+ + + + + + +
+
+
/** @module FacetsComponent */
+
+import Component from '../component';
+import StorageKeys from '../../../core/storage/storagekeys';
+import ResultsContext from '../../../core/storage/resultscontext';
+import ComponentTypes from '../../components/componenttypes';
+import TranslationFlagger from '../../i18n/translationflagger';
+import Facet from '../../../core/models/facet';
+import cloneDeep from 'lodash/cloneDeep';
+
+class FacetsConfig {
+  constructor (config) {
+    /**
+     * The title to display above the controls
+     * @type {string}
+     */
+    this.title = config.title || TranslationFlagger.flag({
+      phrase: 'Filters',
+      context: 'Plural noun, title for a group of controls that filter results'
+    });
+
+    /**
+     * If true, display the number of results next to each facet
+     * @type {boolean}
+     */
+    this.showCount = config.showCount === undefined ? true : config.showCount;
+
+    /**
+     * If true, trigger a search on each change to a filter
+     * @type {boolean}
+     */
+    this.searchOnChange = config.searchOnChange || false;
+
+    /**
+     * If true, show a button to reset for each facet group
+     * @type {boolean}
+     */
+    this.resetFacet = config.resetFacet || false;
+
+    /**
+     * The label to show for the reset button
+     * @type {string}
+     */
+    this.resetFacetLabel = config.resetFacetLabel || TranslationFlagger.flag({
+      phrase: 'reset',
+      context: 'Button label, deselects one or more options'
+    });
+
+    /**
+     * If true, show a "reset all" button to reset all facets
+     * @type {boolean}
+     */
+    this.resetFacets = config.resetFacets;
+
+    /**
+     * The label to show for the "reset all" button
+     * @type {string}
+     */
+    this.resetFacetsLabel = config.resetFacetsLabel || TranslationFlagger.flag({
+      phrase: 'reset all',
+      context: 'Button label, deselects all options'
+    });
+
+    /**
+     * The max number of facets to show before displaying "show more"/"show less"
+     * @type {number}
+     */
+    this.showMoreLimit = config.showMoreLimit || 5;
+
+    /**
+     * The label to show for displaying more facets
+     * @type {string}
+     */
+    this.showMoreLabel = config.showMoreLabel || TranslationFlagger.flag({
+      phrase: 'show more',
+      context: 'Displays more options'
+    });
+
+    /**
+     * The label to show for displaying less facets
+     * @type {string}
+     */
+    this.showLessLabel = config.showLessLabel || TranslationFlagger.flag({
+      phrase: 'show less',
+      context: 'Displays less options'
+    });
+
+    /**
+     * If true, enable hiding excess facets in each group with a "show more"/"show less" button
+     * @type {boolean}
+     */
+    this.showMore = config.showMore === undefined ? true : config.showMore;
+
+    /**
+     * If true, allow expanding and collapsing each group of facets
+     * @type {boolean}
+     */
+    this.expand = config.expand === undefined ? true : config.expand;
+
+    /**
+     * If true, display the number of currently applied filters when collapsed
+     * @type {boolean}
+     */
+    this.showNumberApplied = config.showNumberApplied === undefined ? true : config.showNumberApplied;
+
+    /**
+     * Text to display on the apply button
+     * @type {string}
+     */
+    this.applyLabel = config.applyLabel || TranslationFlagger.flag({
+      phrase: 'apply',
+      context: 'Button label, effectuates changes'
+    });
+
+    /**
+     * The controls to use for each field. Each type of filter has a default
+     * $eq : multioption (checkbox)
+     *
+     * DEPRECATED: use transformFacets instead
+     *
+     * @type {Object}
+     */
+    this.fieldControls = config.fieldControls || {};
+
+    /**
+     * The placeholder text used for the filter option search input
+     * @type {string}
+     */
+    this.placeholderText = config.placeholderText || TranslationFlagger.flag({
+      phrase: 'Search here...',
+      context: 'Placeholder text for input field'
+    });
+
+    /**
+     * If true, display the filter option search input
+     * @type {boolean}
+     */
+    this.searchable = config.searchable || false;
+
+    /**
+     * The form label text for the search input
+     * @type {boolean}
+     */
+    this.searchLabelText = config.searchLabelText || TranslationFlagger.flag({
+      phrase: 'Search for a filter option',
+      context: 'Labels an input field'
+    });
+
+    /**
+     * An object that maps field API names to their filter options overrides,
+     * which have the same keys as the config options in FilterOptions component.
+     *
+     * DEPRECATED: use transformFacets instead
+     *
+     * @type {Object}
+     */
+    this.fields = config.fields || {};
+
+    /**
+     * The selector of the apply button
+     * @type {string}
+     * @private
+     */
+    this.applyButtonSelector = config.applyButtonSelector || null;
+
+    /**
+     * A transformation function which operates on the core library DisplayableFacet model
+     * @type {Function}
+     */
+    this.transformFacets = config.transformFacets;
+
+    this.validate();
+  }
+
+  validate () {
+  }
+}
+
+/**
+ * Displays a set of dynamic filters returned from the backend
+ * @extends Component
+ */
+export default class FacetsComponent extends Component {
+  constructor (config = {}, systemConfig = {}) {
+    super(config, systemConfig);
+
+    this.config = new FacetsConfig(config);
+
+    /**
+     * The vertical key for the search
+     * @type {string}
+     * @private
+     */
+    this._verticalKey = config.verticalKey;
+
+    /**
+     * The selector of the apply button
+     * @type {string}
+     * @private
+     */
+    this._applyButtonSelector = config.applyButtonSelector || null;
+
+    /**
+     * An internal reference for the data storage to listen for updates from the server
+     * @type {string}
+     */
+    this.moduleId = StorageKeys.DYNAMIC_FILTERS;
+
+    /**
+     * The filter box that displays the dynamic filters
+     * @type {FilterBoxComponent}
+     * @private
+     */
+    this._filterbox = null;
+
+    /**
+     * A transformation function which operates on the core library DisplayableFacet model
+     * @type {Function}
+     */
+    this._transformFacets = config.transformFacets;
+  }
+
+  static get type () {
+    return ComponentTypes.FACETS;
+  }
+
+  /**
+   * The template to render
+   * @returns {string}
+   * @override
+   */
+  static defaultTemplateName () {
+    return 'filters/facets';
+  }
+
+  setState (data) {
+    let facets = data['filters'] || [];
+
+    if (this._transformFacets) {
+      const facetsCopy = cloneDeep(facets);
+      facets = this._transformFacets(facetsCopy, this.config);
+    }
+
+    facets = facets.map(this._applyDefaultFormatting);
+
+    return super.setState({
+      ...data,
+      filters: Facet.fromCore(facets),
+      filterOptionsConfigs: this._getFilterOptionsConfigs(facets),
+      isNoResults: data.resultsContext === ResultsContext.NO_RESULTS
+    });
+  }
+
+  /**
+   * Extracts the filter options from transformedFacets and puts them in an object
+   * keyed by fieldId
+   *
+   * @param {DisplayableFacet | FilterOptionsConfig} transformedFacets a union of the
+   * DisplayableFacet model from ansers-core, and the config options of the FilterOptionsConfig
+   * @returns {Object} config options of the FilterOptionsConfig keyed by fieldId
+   */
+  _getFilterOptionsConfigs (transformedFacets) {
+    return transformedFacets.reduce((acc, currentFacet) => {
+      const filterOptions = Object.assign({}, currentFacet);
+      // Delete the options from filterOptions because a DisplayableFacetOption array cannot be
+      // passed to FilterOptionsConfig. Even after deletion here, the filter options will still
+      // exist in the 'filters' field of the facets component state, and therefore any
+      // modifications which occur to options inside transformFacets will still take effect.
+      filterOptions['options'] && delete filterOptions['options'];
+      acc[currentFacet.fieldId] = filterOptions;
+      return acc;
+    }, {});
+  }
+
+  /**
+   * Applies default formatting to a facet
+   *
+   * @param {DisplayableFacet} facet from answers-core
+   * @returns {DisplayableFacet} from answers-core
+   */
+  _applyDefaultFormatting (facet) {
+    const isBooleanFacet = facet => {
+      const firstOption = (facet.options && facet.options[0]) || {};
+      return firstOption['value'] === true || firstOption['value'] === false;
+    };
+
+    if (isBooleanFacet(facet)) {
+      return FacetsComponent._transformBooleanFacet(facet);
+    }
+    return facet;
+  }
+
+  /**
+   * Applies default formatting to a boolean facet
+   *
+   * @param {DisplayableFacet} facet from answers-core
+   * @returns {DisplayableFacet} from answers-core
+   */
+  static _transformBooleanFacet (facet) {
+    const options = facet.options.map(option => {
+      let displayName = option.displayName;
+      if (option.value === true && displayName === 'true') {
+        displayName = TranslationFlagger.flag({ phrase: 'True', context: 'True or False' });
+      }
+      if (option.value === false && displayName === 'false') {
+        displayName = TranslationFlagger.flag({ phrase: 'False', context: 'True or False' });
+      }
+      return Object.assign({}, option, { displayName });
+    });
+    return Object.assign({}, facet, { options });
+  }
+
+  remove () {
+    if (this._filterbox) {
+      this._filterbox.remove();
+    }
+    super.remove();
+  }
+
+  onMount () {
+    this.core.enableDynamicFilters();
+
+    if (this._filterbox) {
+      this._filterbox.remove();
+      this._filterbox = null;
+    }
+
+    let { filters, isNoResults, filterOptionsConfigs } = this._state.get();
+
+    if (filters.length === 0 || isNoResults) {
+      return;
+    }
+
+    filters = filters.map(f => {
+      const fieldOverrides = this.config.transformFacets
+        ? filterOptionsConfigs[f.fieldId] || {}
+        : this.config.fields[f.fieldId] || {};
+
+      return Object.assign({}, f, {
+        type: 'FilterOptions',
+        control: this.config.fieldControls[f.fieldId] || 'multioption',
+        searchable: this.config.searchable,
+        searchLabelText: this.config.searchLabelText,
+        placeholderText: this.config.placeholderText,
+        showExpand: fieldOverrides.expand === undefined ? this.config.expand : fieldOverrides.expand,
+        ...fieldOverrides
+      });
+    });
+
+    // TODO: pass an apply() method to FilterBox, that will override its default behavior,
+    // and remove the isDynamic config option.
+    this._filterbox = this.componentManager.create(
+      'FilterBox',
+      Object.assign({}, this.config, {
+        parentContainer: this._container,
+        name: `${this.name}.filterbox`,
+        container: '.js-yxt-Facets',
+        verticalKey: this._verticalKey,
+        resetFilter: this.config.resetFacet,
+        resetFilters: this.config.resetFacets,
+        resetFilterLabel: this.config.resetFacetLabel,
+        resetFiltersLabel: this.config.resetFacetsLabel,
+        isDynamic: true,
+        filters
+      })
+    );
+
+    this._filterbox.mount();
+    this.core.storage.set(StorageKeys.FACETS_LOADED, true);
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/ui_components_filters_filterboxcomponent.js.html b/docs/ui_components_filters_filterboxcomponent.js.html index bcaa275bc..60ecdcd44 100644 --- a/docs/ui_components_filters_filterboxcomponent.js.html +++ b/docs/ui_components_filters_filterboxcomponent.js.html @@ -31,8 +31,139 @@

Source: ui/components/filters/filterboxcomponent.js

import Component from '../component'; import { AnswersComponentError } from '../../../core/errors/errors'; import DOM from '../../dom/dom'; -import StorageKeys from '../../../core/storage/storagekeys'; -import Filter from '../../../core/models/filter'; +import ComponentTypes from '../../components/componenttypes'; +import TranslationFlagger from '../../i18n/translationflagger'; +import QueryTriggers from '../../../core/models/querytriggers'; + +class FilterBoxConfig { + constructor (config) { + /** + * The title to display above the controls + * @type {string} + */ + this.title = config.title || TranslationFlagger.flag({ + phrase: 'Filters', + context: 'Plural noun, title for a group of controls that filter results' + }); + + /** + * If true, display the number of results next to each facet + * @type {boolean} + */ + this.showCount = config.showCount === undefined ? true : config.showCount; + + /** + * If true, trigger a search on each change to a filter + * @type {boolean} + */ + this.searchOnChange = config.searchOnChange || false; + + /** + * If true, show a button to reset for each facet group + * @type {boolean} + */ + this.resetFilter = config.resetFilter || false; + + /** + * The label to show for the reset button + * @type {string} + */ + this.resetFilterLabel = config.resetFilterLabel || TranslationFlagger.flag({ + phrase: 'reset', + context: 'Button label, deselects one or more options' + }); + + /** + * If true, show a "reset all" button to reset all facets + * @type {boolean} + */ + this.resetFilters = config.resetFilters === undefined ? !config.searchOnChange : config.resetFilters; + + /** + * The label to show for the "reset all" button + * @type {string} + */ + this.resetFiltersLabel = config.resetFiltersLabel || TranslationFlagger.flag({ + phrase: 'reset all', + context: 'Button label, deselects all options' + }); + + /** + * The max number of facets to show before displaying "show more"/"show less" + * @type {number} + */ + this.showMoreLimit = config.showMoreLimit || 5; + + /** + * The label to show for displaying more facets + * @type {string} + */ + this.showMoreLabel = config.showMoreLabel || TranslationFlagger.flag({ + phrase: 'show more', + context: 'Displays more options' + }); + + /** + * The label to show for displaying less facets + * @type {string} + */ + this.showLessLabel = config.showLessLabel || TranslationFlagger.flag({ + phrase: 'show less', + context: 'Displays less options' + }); + + /** + * If true, enable hiding excess facets in each group with a "show more"/"show less" button + * @type {boolean} + */ + this.showMore = config.showMore === undefined ? true : config.showMore; + + /** + * If true, allow expanding and collapsing each group of facets + * @type {boolean} + */ + this.expand = config.expand === undefined ? true : config.expand; + + /** + * If true, display the number of currently applied filters when collapsed + * @type {boolean} + */ + this.showNumberApplied = config.showNumberApplied === undefined ? true : config.showNumberApplied; + + /** + * Text to display on the apply button + * @type {string} + */ + this.applyLabel = config.applyLabel || TranslationFlagger.flag({ + phrase: 'apply', + context: 'Button label, effectuates changes' + }); + + /** + * The selector of the apply button + * @type {string} + */ + this.applyButtonSelector = config.applyButtonSelector || '.js-yext-filterbox-apply'; + + /** + * The list of filters to display and control, ignoring empty sections + * @type {object[]} + */ + this.filterConfigs = config.filters.filter(f => f.options.length); + + /** + * Whether or not this filterbox contains facets. This affects the + * the way the filters are used in the search + * @type {boolean} + */ + this.isDynamic = config.isDynamic || false; + + this.validate(); + } + + validate () { + } +} /** * Renders a set of filters, and searches with them when applied. @@ -40,8 +171,10 @@

Source: ui/components/filters/filterboxcomponent.js

* @extends Component */ export default class FilterBoxComponent extends Component { - constructor (config = {}) { - super(config); + constructor (config = {}, systemConfig = {}) { + super(config, systemConfig); + + this.config = new FilterBoxConfig(config); if (!config.filters || !(config.filters instanceof Array)) { throw new AnswersComponentError( @@ -49,13 +182,6 @@

Source: ui/components/filters/filterboxcomponent.js

'FilterBox'); } - /** - * The list of filters to display and control - * @type {object[]} - * @private - */ - this._filterConfigs = config.filters; - /** * The vertical key for the search * @type {string} @@ -63,20 +189,6 @@

Source: ui/components/filters/filterboxcomponent.js

*/ this._verticalKey = config.verticalKey || null; - /** - * If true, trigger a search on each change to a filter - * @type {boolean} - * @private - */ - this._searchOnChange = config.searchOnChange || false; - - /** - * The selector of the apply button - * @type {string} - * @private - */ - this._applyButtonSelector = config.applyButtonSelector || '.js-yext-filterbox-apply'; - /** * The components created for each filter config * @type {Component[]} @@ -86,96 +198,139 @@

Source: ui/components/filters/filterboxcomponent.js

/** * The current state of the filter components in the box - * @type {Filter} + * @type {Array<FilterNode>} * @private */ - this._filters = []; + this._filterNodes = []; - /** - * The template to render - * @type {string} - * @private - */ - this._templateName = `filters/filterbox`; + this.config.filterConfigs.forEach(config => { + let hideCount = config.showCount === undefined ? !this.config.showCount : !config.showCount; + + if (hideCount) { + config.options.forEach(option => { + option.countLabel = null; + }); + } + }); } static get type () { - return 'FilterBox'; + return ComponentTypes.FILTER_BOX; + } + + static defaultTemplateName () { + return 'filters/filterbox'; } setState (data) { - super.setState(Object.assign(data, { - filterConfigs: this._filterConfigs, - showApplyButton: !this._searchOnChange + super.setState(Object.assign({}, data, this.config, { + showReset: this.config.resetFilters, + resetLabel: this.config.resetFiltersLabel, + showApplyButton: !this.config.searchOnChange })); } onMount () { + if (this._filterComponents.length) { + this._filterComponents.forEach(c => c.remove()); + this._filterComponents = []; + this._filterNodes = []; + } + // Initialize filters from configs - for (let i = 0; i < this._filterConfigs.length; i++) { - const config = this._filterConfigs[i]; - const component = this.componentManager.create(config.type, Object.assign({}, - config, - { - parent: this, - name: `${this.name}.filter${i}`, - storeOnChange: false, - container: `.js-yext-filterbox-filter${i}`, - onChange: (f) => { - this.onFilterChange(i, f); - } - })); + for (let i = 0; i < this.config.filterConfigs.length; i++) { + const config = this.config.filterConfigs[i]; + const component = this.componentManager.create(config.type, { + ...this.config, + parentContainer: this._container, + name: `${this.name}.filter${i}`, + storeOnChange: false, + container: `.js-yext-filterbox-filter${i}`, + showReset: this.config.resetFilter, + resetLabel: this.config.resetFilterLabel, + isDynamic: this.config.isDynamic, + ...config, + showExpand: config.showExpand === undefined ? this.config.expand : config.showExpand, + onChange: (filterNode, alwaysSaveFilterNodes, blockSearchOnChange) => { + const _saveFilterNodes = this.config.searchOnChange || alwaysSaveFilterNodes; + const _searchOnChange = this.config.searchOnChange && !blockSearchOnChange; + this.onFilterNodeChange(i, filterNode, _saveFilterNodes, _searchOnChange); + config.onChange && config.onChange(); + } + }); + if (this.config.isDynamic && typeof component.floatSelected === 'function') { + component.floatSelected(); + } component.mount(); this._filterComponents.push(component); + this._filterNodes[i] = component.getFilterNode(); } + this._saveFilterNodesToStorage(); // Initialize apply button - if (!this._searchOnChange) { - const button = DOM.query(this._container, this._applyButtonSelector); - DOM.on(button, 'click', () => { - this._saveFiltersToStorage(); - this._search(); - }); + if (!this.config.searchOnChange) { + const button = DOM.query(this._container, this.config.applyButtonSelector); + + if (button) { + DOM.on(button, 'click', () => { + this._saveFilterNodesToStorage(); + this.core.triggerSearch(QueryTriggers.FILTER_COMPONENT); + }); + } } + + // Initialize reset button + let resetEl = DOM.query(this._container, '.js-yxt-FilterBox-reset'); + + if (resetEl) { + DOM.on(resetEl, 'click', this.resetFilters.bind(this)); + } + } + + _getValidFilterNodes () { + return this._filterNodes.filter(fn => fn.getFilter().getFilterKey()); + } + + resetFilters () { + this._filterComponents.forEach(filter => filter.clearOptions()); } /** * Handle changes to child filter components * @param {number} index The index of the changed filter - * @param {Filter} filter The new filter + * @param {FilterNode} filterNode The new filter node + * @param {boolean} saveFilterNodes Whether to save filternodes to storage + * @param {boolean} searchOnChange Whether to conduct a search */ - onFilterChange (index, filter) { - this._filters[index] = filter; - if (this._searchOnChange) { - this._saveFiltersToStorage(); - this._search(); + onFilterNodeChange (index, filterNode, saveFilterNodes, searchOnChange) { + this._filterNodes[index] = filterNode; + if (saveFilterNodes || searchOnChange) { + this._saveFilterNodesToStorage(); + } + if (searchOnChange) { + this.core.triggerSearch(QueryTriggers.FILTER_COMPONENT); } } /** - * Save current filters to storage to be used in the next search - * @private + * Remove all filter components along with this component */ - _saveFiltersToStorage () { - const validFilters = this._filters.filter(f => f !== undefined && f !== null); - const combinedFilter = validFilters.length > 1 - ? Filter.and(...validFilters) - : validFilters[0]; - this.core.setFilter(this.name, combinedFilter || {}); + remove () { + this._filterComponents.forEach(c => c.remove()); + super.remove(); } /** - * Trigger a search with all filters in storage + * Save current filters to storage to be used in the next search + * @private */ - _search () { - const allFilters = this.core.storage.getAll(StorageKeys.FILTER); - const totalFilter = allFilters.length > 1 - ? Filter.and(...allFilters) - : allFilters[0]; - - const query = this.core.storage.getState(StorageKeys.QUERY) || ''; - - this.core.verticalSearch(query, this._verticalKey, JSON.stringify(totalFilter)); + _saveFilterNodesToStorage () { + if (this.config.isDynamic) { + const availableFieldIds = this.config.filterConfigs.map(config => config.fieldId); + this.core.setFacetFilterNodes(availableFieldIds, this._getValidFilterNodes()); + } else { + this._filterComponents.forEach(fc => fc.apply()); + } } }
@@ -188,13 +343,13 @@

Source: ui/components/filters/filterboxcomponent.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/ui_components_filters_filteroptionscomponent.js.html b/docs/ui_components_filters_filteroptionscomponent.js.html index 6997b572c..3a495d7fc 100644 --- a/docs/ui_components_filters_filteroptionscomponent.js.html +++ b/docs/ui_components_filters_filteroptionscomponent.js.html @@ -32,6 +32,16 @@

Source: ui/components/filters/filteroptionscomponent.jsSource: ui/components/filters/filteroptionscomponent.js Object.assign({}, o, { checked: false })); + this.initialOptions = config.options.map(o => ({ ...o })); /** - * The type of control to display + * The list of filter options to display. + * @type {object[]} + */ + this.options = config.options.map(o => ({ ...o })); + + /** + * The label to be used in the legend * @type {string} - * @private */ - this._control = config.control; + this.label = config.label || TranslationFlagger.flag({ + phrase: 'Filters', + context: 'Plural noun, title for a group of controls that filter results' + }); /** - * The selector used for options in the template + * The callback function to call when changed + * @type {function} + */ + this.onChange = config.onChange || function () { }; + + /** + * If true, stores the filter to global and persistent storage on each change + * @type {boolean} + */ + this.storeOnChange = config.storeOnChange === undefined ? true : config.storeOnChange; + + /** + * If true, show a button to reset the current filter selection + * @type {boolean} + */ + this.showReset = config.showReset && this.options.length > 0; + + /** + * Whether this FilterOptions is part of a dynamic FilterBox component (i.e. is + * part of a FacetsComponent). Used to correctly set the {@link FilterType} of + * the created {@link FilterNode}. + * @type {boolean} + */ + this.isDynamic = config.isDynamic; + + /** + * The label to show for the reset button + * @type {string} + */ + this.resetLabel = config.resetLabel || TranslationFlagger.flag({ + phrase: 'reset', + context: 'Button label, deselects one or more options' + }); + + /** + * The max number of facets to show before displaying "show more"/"show less" + * @type {number} + */ + this.showMoreLimit = config.showMoreLimit || 5; + + /** + * The label to show for displaying more facets * @type {string} - * @private */ - this._optionSelector = config.optionSelector || '.js-yext-filter-option'; + this.showMoreLabel = config.showMoreLabel || TranslationFlagger.flag({ + phrase: 'show more', + context: 'Displays more options' + }); /** - * If true, stores the filter to storage on each change + * The label to show for displaying less facets + * @type {string} + */ + this.showLessLabel = config.showLessLabel || TranslationFlagger.flag({ + phrase: 'show less', + context: 'Displays less options' + }); + + /** + * If true, enable hiding excess facets with a "show more"/"show less" button * @type {boolean} - * @private */ - this._storeOnChange = config.storeOnChange || false; + this.showMore = config.showMore === undefined ? true : config.showMore; + this.showMore = this.showMore && this.options.length > this.showMoreLimit; /** - * The callback function to call when changed - * @type {function} - * @private + * If true, allow expanding and collapsing the group of facets + * @type {boolean} + */ + this.showExpand = config.showExpand === undefined ? true : config.showExpand; + + /** + * If true, display the number of currently applied filters when collapsed + * @type {boolean} + */ + this.showNumberApplied = config.showNumberApplied === undefined ? true : config.showNumberApplied; + + /** + * The selector used for options in the template + * @type {string} */ - this._onChange = config.onChange || function () {}; + this.optionSelector = config.optionSelector || '.js-yext-filter-option'; /** - * The template to render, based on the control + * The placeholder text used for the filter option search input * @type {string} - * @private */ - this._templateName = `controls/${config.control}`; + this.placeholderText = config.placeholderText || TranslationFlagger.flag({ + phrase: 'Search here...', + context: 'Placeholder text for input field' + }); + + /** + * If true, display the filter option search input + * @type {boolean} + */ + this.searchable = config.searchable || false; + + /** + * The form label text for the search input + * @type {boolean} + */ + this.searchLabelText = config.searchLabelText || TranslationFlagger.flag({ + phrase: 'Search for a filter option', + context: 'Labels an input field' + }); + + this.validate(); + const { persistedFilter, persistedLocationRadius } = persistedState; + if (!this.isDynamic) { + const hasPersistedLocationRadius = persistedLocationRadius || persistedLocationRadius === 0; + if (this.optionType === OptionTypes.STATIC_FILTER && persistedFilter) { + this.options = this.getPersistedStaticFilterOptions(this.options, persistedFilter); + } else if (this.optionType === OptionTypes.RADIUS_FILTER && hasPersistedLocationRadius) { + this.options = this.getPersistedLocationRadiusOptions(this.options, persistedLocationRadius); + } + } + } + + /** + * Returns the initial options from config, but with the persisted filters set to + * selected = true. + * + * @param {Array<{{ + * label: string, + * value: string, + * field: string, + * selected?: boolean + * }}>} initialOptions Options from the component configuration. + * @param {Object} persistedFilter A persisted filter, can be combined or simple + * @returns {Array<Object>} The options in the same format as initialOptions with updated + * selected values + */ + getPersistedStaticFilterOptions (initialOptions, persistedFilter) { + return initialOptions.map(o => { + const filterForOption = Filter.equal(o.field, o.value); + const isPersisted = filterIsPersisted(filterForOption, persistedFilter); + return { + ...o, + selected: isPersisted + }; + }); + } + + /** + * Returns the initial options from config, but with the persisted location radius filter + * set to selected = true. + * + * @param {Array<{{ + * label: string, + * value: string, + * selected?: boolean + * }}>} initialOptions Options from the component configuration. + * @param {number} persistedLocationRadius The value of the persisted locationRadius + * @returns {Array<Object>} The options in the same format as initialOptions with updated + * selected values + */ + getPersistedLocationRadiusOptions (initialOptions, persistedLocationRadius) { + return initialOptions.map(o => { + const isPersisted = o.value === persistedLocationRadius; + return { + ...o, + selected: isPersisted + }; + }); + } + + getInitialSelectedCount () { + return this.options.reduce( + (numSelected, option) => option.selected ? numSelected + 1 : numSelected, + 0); + } + + validate () { + if (!this.control || !SUPPORTED_CONTROLS.includes(this.control)) { + throw new AnswersComponentError( + 'FilterOptions requires a valid "control" to be provided', + 'FilterOptions'); + } + + if (!(this.optionType in OptionTypes)) { + const possibleTypes = Object.values(OptionTypes).join(', '); + throw new AnswersComponentError( + `Invalid optionType ${this.optionType} passed to FilterOptions. Expected one of ${possibleTypes}`, + 'FilterOptions'); + } + + if (this.optionType === OptionTypes.RADIUS_FILTER && this.control !== 'singleoption') { + throw new AnswersComponentError( + `FilterOptions of optionType ${OptionTypes.RADIUS_FILTER} requires control "singleoption"`, + 'FilterOptions'); + } + + if (!this.options) { + throw new AnswersComponentError( + 'FilterOptions component requires options to be provided', + 'FilterOptions'); + } + + if (this.control === 'singleoption' && this.options.filter(o => o.selected).length > 1) { + throw new AnswersComponentError( + 'FilterOptions component with "singleoption" control cannot have multiple selected options', + 'FilterOptions'); + } + } +} + +/** + * Renders a set of options, each one representing a filter in a search. + */ +export default class FilterOptionsComponent extends Component { + constructor (config = {}, systemConfig = {}) { + super(config, systemConfig); + this._initVariables(config); + + if (this.config.storeOnChange) { + this.apply(); + } + + if (!this.config.isDynamic) { + this._registerBackNavigationListener(); + } + } + + /** + * Initializes the component's instance variables. + * + * @param {Object} config + */ + _initVariables (config) { + const persistedFilter = this.core.storage.get(StorageKeys.PERSISTED_FILTER); + const persistedLocationRadius = this.core.storage.get(StorageKeys.PERSISTED_LOCATION_RADIUS); + const persistedState = { persistedFilter, persistedLocationRadius }; + + /** + * The component config + * @type {FilterOptionsConfig} + */ + this.config = new FilterOptionsConfig(config, persistedState); + + const selectedCount = this.config.getInitialSelectedCount(); + + /** + * True if the option list is expanded and visible + * @type {boolean} + */ + this.expanded = this.config.showExpand ? selectedCount > 0 : true; + + /** + * Whether the current is currently showing more or less. If true, is currently "show more". + * Only used if config.showMore is true. + * @type {boolean} + */ + this.showMoreState = this.config.showMore; + } + + _registerBackNavigationListener () { + this.core.storage.registerListener({ + eventType: 'update', + storageKey: StorageKeys.HISTORY_POP_STATE, + callback: () => { + this._initVariables(this._config); + this.updateListeners(true, true); + this.setState(); + } + }); } static get type () { - return 'FilterOptions'; + return ComponentTypes.FILTER_OPTIONS; + } + + /** + * The template to render, based on the control + * @returns {string} + * @override + */ + static defaultTemplateName (config) { + return `controls/filteroptions`; } setState (data) { + const selectedCount = this._getSelectedCount(); super.setState(Object.assign({}, data, { - name: this.name, - options: this._options + name: this.name.toLowerCase(), + ...this.config, + showMoreState: this.showMoreState, + displayReset: this.config.showReset && selectedCount > 0, + expanded: this.expanded, + selectedCount, + isSingleOption: this.config.control === 'singleoption' })); } onMount () { - DOM.delegate(this._container, this._optionSelector, 'click', (event) => { - this._updateOption(parseInt(event.target.dataset.index), event.target.checked); + DOM.delegate( + DOM.query(this._container, `.yxt-FilterOptions-options`), + this.config.optionSelector, + 'click', + event => { + let selectedCountEl = DOM.query(this._container, '.js-yxt-FilterOptions-selectedCount'); + if (selectedCountEl) { + selectedCountEl.innerText = this._getSelectedCount(); + } + this._updateOption(parseInt(event.target.dataset.index), event.target.checked); + }); + + // Initialize reset element if present + const resetEl = DOM.query(this._container, '.js-yxt-FilterOptions-reset'); + if (resetEl) { + DOM.on(resetEl, 'click', this.clearOptions.bind(this)); + } - const filter = this._buildFilter(); - if (this._storeOnChange) { - this.core.setFilter(this.name, filter); + // show more/less button + if (this.config.showMore) { + const showLessEl = DOM.query(this._container, '.js-yxt-FilterOptions-showLess'); + const showMoreEl = DOM.query(this._container, '.js-yxt-FilterOptions-showMore'); + const optionsOverLimitEls = DOM.queryAll(this._container, '.js-yxt-FilterOptions-aboveShowMoreLimit'); + DOM.on( + showLessEl, + 'click', + () => { + this.showMoreState = true; + showLessEl.classList.add('hidden'); + showMoreEl.classList.remove('hidden'); + for (let optionEl of optionsOverLimitEls) { + optionEl.classList.add('hidden'); + } + }); + DOM.on( + showMoreEl, + 'click', + () => { + this.showMoreState = false; + showLessEl.classList.remove('hidden'); + showMoreEl.classList.add('hidden'); + for (let optionEl of optionsOverLimitEls) { + optionEl.classList.remove('hidden'); + } + }); + } + + // searchable option list + if (this.config.searchable) { + const clearSearchEl = DOM.query(this._container, '.js-yxt-FilterOptions-clearSearch'); + const searchInputEl = DOM.query(this._container, '.js-yxt-FilterOptions-filter'); + const filterOptionEls = DOM.queryAll(this._container, '.js-yxt-FilterOptions-option'); + const filterContainerEl = DOM.query(this._container, '.js-yxt-FilterOptions-container'); + + // On clearSearchEl click, clear search input + if (clearSearchEl && searchInputEl) { + DOM.on(clearSearchEl, 'click', event => { + searchInputEl.value = ''; + DOM.trigger(searchInputEl, 'input'); + searchInputEl.focus(); + }); } - this._onChange(filter); - }); + DOM.on( + searchInputEl, + 'input', + event => { + const filter = event.target.value; + + if (!filter) { + filterContainerEl.classList.remove('yxt-FilterOptions-container--searching'); + clearSearchEl.classList.add('js-hidden'); + } else { + filterContainerEl.classList.add('yxt-FilterOptions-container--searching'); + clearSearchEl.classList.remove('js-hidden'); + } + + for (let filterOption of filterOptionEls) { + const labelEl = DOM.query(filterOption, '.js-yxt-FilterOptions-optionLabel--name'); + let labelText = labelEl.textContent || labelEl.innerText || ''; + labelText = labelText.trim(); + if (!filter) { + filterOption.classList.remove('hiddenSearch'); + filterOption.classList.remove('displaySearch'); + labelEl.innerHTML = labelText; + } else { + let matchedSubstring = this._getMatchedSubstring(labelText.toLowerCase(), filter.toLowerCase()); + if (matchedSubstring) { + filterOption.classList.add('displaySearch'); + filterOption.classList.remove('hiddenSearch'); + labelEl.innerHTML = new HighlightedValue({ + value: labelText, + matchedSubstrings: [matchedSubstring] + }).get(); + } else { + filterOption.classList.add('hiddenSearch'); + filterOption.classList.remove('displaySearch'); + labelEl.innerHTML = labelText; + } + } + } + } + ); + } + + // expand button + if (this.config.showExpand) { + const legend = DOM.query(this._container, '.yxt-FilterOptions-clickableLegend'); + DOM.on( + legend, + 'mousedown', + click => { + if (click.button === 0) { + this.expanded = !this.expanded; + this.setState(); + } + }); + + DOM.on( + legend, + 'keydown', + key => { + if (key.key === ' ' || key.key === 'Enter') { + key.preventDefault(); + this.expanded = !this.expanded; + this.setState(); + } + }); + } + } + + /** + * Returns the count of currently selected options + * @returns {number} + * @private + */ + _getSelectedCount () { + return this.config.options.filter(o => o.selected).length; } - _updateOption (index, checked) { - if (this._control === 'singleoption') { - this._options = this._options.map(o => Object.assign({}, o, { checked: false })); + /** + * Toggles the display of the reset element based on the selected count. If there are selected + * options, show the reset element, if not, hide it. + * + * Note: this will not have any effect if the reset element isn't in the DOM. + * + * @returns {number} + * @private + */ + _toggleReset () { + const resetEl = DOM.query(this._container, '.js-yxt-FilterOptions-reset'); + const selectedCount = this._getSelectedCount(); + if (selectedCount > 0) { + resetEl.classList.remove('js-hidden'); + } else if (!resetEl.classList.contains('js-hidden')) { + resetEl.classList.add('js-hidden'); } + } - this._options[index] = Object.assign({}, this._options[index], { checked }); - this.setState(); + /** + * Finds the length and offset of the substring where (string) option and + * (string) filter "match". + * + * "Match" is defined as an exact text match, or -- if the length of filter + * is greater than the `minFilterSizeForLevenshtein` -- a "match" can occur if + * any "n length" substring of option (where "n length" is the length of filter) + * is within the `maxLevenshteinDistance` levenshtein distance of the filter. + * + * Note: this is case sensitive. + * + * @returns {Object} + * @private + */ + _getMatchedSubstring (option, filter) { + let offset = this._getOffset(option, filter); + if (offset > -1) { + return { + length: filter.length, + offset: offset + }; + } + + const minFilterSizeForLevenshtein = 3; + const maxLevenshteinDistance = 1; + if (filter.length > minFilterSizeForLevenshtein) { + // Break option into X filter.length size substrings + let substrings = []; + for (let start = 0; start <= (option.length - filter.length); start++) { + substrings.push(option.substr(start, filter.length)); + } + + // Find the substring that is the closest in levenshtein distance to filter + let minLevDist = filter.length; + let minLevSubstring = filter; + for (let substring of substrings) { + let levDist = this._calcLevenshteinDistance(substring, filter); + if (levDist < minLevDist) { + minLevDist = levDist; + minLevSubstring = substring; + } + } + + // If the min levenshtein distance is below the max, count it as a match + if (minLevDist <= maxLevenshteinDistance) { + offset = this._getOffset(option, minLevSubstring); + if (offset > -1) { + return { + length: filter.length, + offset: offset + }; + } + } + } } /** - * Clear all options + * Calculate the levenshtein distance for two strings + * @returns {number} + * @private */ - clear () { - const elements = DOM.queryAll(this._container, this._optionSelector); - elements.forEach(e => e.setAttribute('checked', 'false')); - this._applyFilter(); + _calcLevenshteinDistance (a, b) { + return levenshtein(a, b); } /** - * Build and return the Filter that represents the current state - * @returns {Filter} + * Returns the starting index of first occurance of the (string) filter in + * the (string) option, or -1 if not present + * @returns {number} * @private */ - _buildFilter () { - const filters = this._options - .filter(o => o.checked) - .map(o => Filter.equal(o.field, o.value)); + _getOffset (option, filter) { + return (option && filter) ? option.indexOf(filter) : -1; + } + + /** + * Clears all selected options. + */ + clearOptions () { + this.config.options = this.config.options.map(o => Object.assign({}, o, { selected: false })); + this.updateListeners(); + this.setState(); + } + + /** + * Call the config.onChange callback with the FilterNode corresponding to the + * component state. + * @param {boolean} alwaysSaveFilterNodes + * @param {boolean} blockSearchOnChange + */ + updateListeners (alwaysSaveFilterNodes, blockSearchOnChange) { + const filterNode = this.getFilterNode(); + if (this.config.storeOnChange) { + this.apply(); + } + + this.config.onChange(filterNode, alwaysSaveFilterNodes, blockSearchOnChange); + } + + _updateOption (index, selected) { + if (this.config.control === 'singleoption') { + this.config.options = this.config.options.map(o => Object.assign({}, o, { selected: false })); + } + + this.config.options[index] = Object.assign({}, this.config.options[index], { selected }); + + if (this.config.showReset) { + this._toggleReset(); + } + this.updateListeners(); + } + + /** + * Apply filter changes + */ + apply () { + switch (this.config.optionType) { + case OptionTypes.RADIUS_FILTER: + this.core.setLocationRadiusFilterNode(this.getLocationRadiusFilterNode()); + break; + case OptionTypes.STATIC_FILTER: + this.core.setStaticFilterNodes(this.name, this.getFilterNode()); + break; + default: + throw new AnswersComponentError(`Unknown optionType ${this.config.optionType}`, 'FilterOptions'); + } + } + + floatSelected () { + this.config.options = this.config.options.sort((a, b) => b.selected - a.selected); + } + + _buildFilter (option) { + return option.filter ? option.filter : Filter.equal(option.field, option.value); + } + + _getFilterType () { + if (this.config.isDynamic) { + return FilterType.FACET; + } + return this.config.optionType === 'RADIUS_FILTER' + ? FilterType.RADIUS + : FilterType.STATIC; + } + + _buildFilterMetadata (option) { + return new FilterMetadata({ + fieldName: this.config.label, + displayValue: option.label, + filterType: this._getFilterType() + }); + } + + /** + * Return the FilterNode when this is a RADIUS_FILTER. + * @type {FilterNode} + */ + getLocationRadiusFilterNode () { + const selectedOption = this.config.options.find(o => o.selected); + if (!selectedOption) { + return FilterNodeFactory.from(); + } + const filterNode = { + metadata: this._buildFilterMetadata(selectedOption), + filter: { value: selectedOption.value }, + remove: () => this._clearSingleOption(selectedOption) + }; + return FilterNodeFactory.from(filterNode); + } + + _clearSingleOption (option) { + option.selected = false; + this.updateListeners(true, true); + this.setState(); + } + + /** + * Returns this component's filter node when it is a STATIC_FILTER. + * This method is exposed so that components like {@link FilterBoxComponent} + * can access them. + * @returns {FilterNode} + */ + getFilterNode () { + const filterNodes = this.config.options + .filter(o => o.selected) + .map(o => FilterNodeFactory.from({ + filter: this._buildFilter(o), + metadata: this._buildFilterMetadata(o), + remove: () => this._clearSingleOption(o) + })); + + const fieldIdToFilterNodes = groupArray(filterNodes, fn => fn.getFilter().getFilterKey()); + + // OR together filter nodes for the same field id. + const totalFilterNodes = []; + for (const sameIdNodes of Object.values(fieldIdToFilterNodes)) { + totalFilterNodes.push(FilterNodeFactory.or(...sameIdNodes)); + } - return filters.length > 0 - ? Filter.group(...filters) - : {}; + // AND all of the ORed together nodes. + return FilterNodeFactory.and(...totalFilterNodes); } } @@ -171,13 +764,13 @@

Source: ui/components/filters/filteroptionscomponent.js
- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/ui_components_filters_geolocationcomponent.js.html b/docs/ui_components_filters_geolocationcomponent.js.html new file mode 100644 index 000000000..1b2803285 --- /dev/null +++ b/docs/ui_components_filters_geolocationcomponent.js.html @@ -0,0 +1,382 @@ + + + + + JSDoc: Source: ui/components/filters/geolocationcomponent.js + + + + + + + + + + +
+ +

Source: ui/components/filters/geolocationcomponent.js

+ + + + + + +
+
+
/** @module GeoLocationComponent */
+
+import Component from '../component';
+import DOM from '../../dom/dom';
+import Filter from '../../../core/models/filter';
+import StorageKeys from '../../../core/storage/storagekeys';
+import buildSearchParameters from '../../tools/searchparamsparser';
+import FilterNodeFactory from '../../../core/filters/filternodefactory';
+import ComponentTypes from '../../components/componenttypes';
+import TranslationFlagger from '../../i18n/translationflagger';
+import QueryTriggers from '../../../core/models/querytriggers';
+
+const METERS_PER_MILE = 1609.344;
+
+const DEFAULT_CONFIG = {
+  /**
+   * The radius, in miles, around the user's location to find results.
+   * If location accuracy is low, a larger radius may be used automatically
+   * @type {number}
+   */
+  radius: 50,
+
+  /**
+   * The vertical key to use
+   * @type {string}
+   */
+  verticalKey: null,
+
+  /**
+   * If true, submits a search when the value is changed
+   * @type {boolean}
+   */
+  searchOnChange: false,
+
+  /**
+   * The title to display
+   * @type {string}
+   */
+  title: TranslationFlagger.flag({
+    phrase: 'Location'
+  }),
+
+  /**
+   * The label to display
+   * @type {string}
+   */
+  label: TranslationFlagger.flag({
+    phrase: 'Location'
+  }),
+
+  /**
+   * The icon url to show in the geo button
+   * @type {string}
+   */
+  geoButtonIcon: '',
+
+  /**
+   * The alt text to include with the geo button icon
+   * @type {string}
+   */
+  geoButtonIconAltText: TranslationFlagger.flag({
+    phrase: 'Use My Location'
+  }),
+
+  /**
+   * The text to show in the geo button
+   * @type {string}
+   */
+  geoButtonText: TranslationFlagger.flag({
+    phrase: 'Use My Location'
+  }),
+
+  /**
+   * The text to show when geolocation is enabled
+   * @type {string}
+   */
+  enabledText: TranslationFlagger.flag({
+    phrase: 'Current Location',
+    context: 'Labels the user\'s current location'
+  }),
+
+  /**
+   * The text to show when loading the user's location
+   * @type {string}
+   */
+  loadingText: TranslationFlagger.flag({
+    phrase: 'Finding Your Location...'
+  }),
+
+  /**
+   * The text to show if the user's location cannot be found
+   * @type {string}
+   */
+  errorText: TranslationFlagger.flag({
+    phrase: 'Could Not Find Your Location'
+  }),
+
+  /**
+   * The CSS selector of the toggle button
+   * @type {string}
+   */
+  buttonSelector: '.js-yxt-GeoLocationFilter-button',
+
+  /**
+   * The CSS selector of the query input
+   * @type {string}
+   */
+  inputSelector: '.js-yxt-GeoLocationFilter-input'
+};
+
+/**
+ * Renders a button that when clicked adds a static filter representing the user's location
+ * @extends Component
+ */
+export default class GeoLocationComponent extends Component {
+  constructor (config = {}, systemConfig = {}) {
+    super({ ...DEFAULT_CONFIG, ...config }, systemConfig);
+
+    /**
+     * The query string to use for the input box, provided to template for rendering.
+     * @type {string}
+     */
+    this.query = this.core.storage.get(`${StorageKeys.QUERY}.${this.name}`) || '';
+    this.core.storage.registerListener({
+      eventType: 'update',
+      storageKey: `${StorageKeys.QUERY}.${this.name}`,
+      callback: q => {
+        this.query = q;
+        this.setState();
+      }
+    });
+
+    this.searchParameters = buildSearchParameters(config.searchParameters);
+
+    /**
+     * Options to pass to the geolocation api.
+     * @type {Object}
+     */
+    this._geolocationOptions = {
+      enableHighAccuracy: false,
+      timeout: 6000,
+      maximumAge: 300000,
+      ...config.geolocationOptions
+    };
+
+    /**
+     * Options for the geolocation timeout alert.
+     * @type {Object}
+     */
+    this._geolocationTimeoutAlert = {
+      enabled: false,
+      message: TranslationFlagger.flag({
+        phrase: 'We are unable to determine your location'
+      }),
+      ...config.geolocationTimeoutAlert
+    };
+  }
+
+  static get type () {
+    return ComponentTypes.GEOLOCATION_FILTER;
+  }
+
+  static defaultTemplateName () {
+    return 'controls/geolocation';
+  }
+
+  setState (data = {}) {
+    let placeholder = '';
+    if (this._enabled) {
+      placeholder = this._config.enabledText;
+    }
+    if (data.geoLoading) {
+      placeholder = this._config.loadingText;
+    }
+    if (data.geoError) {
+      placeholder = this._config.errorText;
+    }
+    super.setState({
+      ...data,
+      title: this._config.title,
+      geoEnabled: this._enabled,
+      query: this.query,
+      labelText: this._config.label,
+      enabledText: this._config.enabledText,
+      loadingText: this._config.loadingText,
+      errorText: this._config.errorText,
+      geoButtonIcon: this._config.geoButtonIcon,
+      geoValue: this._enabled || data.geoLoading || data.geoError ? '' : this.query,
+      geoPlaceholder: placeholder,
+      geoButtonText: this._config.geoButtonText
+    });
+  }
+
+  onMount () {
+    if (this._autocomplete) {
+      this._autocomplete.remove();
+    }
+
+    this._initAutoComplete(this._config.inputSelector);
+    DOM.on(
+      DOM.query(this._container, this._config.buttonSelector),
+      'click',
+      () => this._toggleGeoFilter()
+    );
+  }
+
+  /**
+   * A helper method to wire up our auto complete on an input selector
+   * @param {string} inputSelector CSS selector to bind our auto complete component to
+   * @private
+   */
+  _initAutoComplete (inputSelector) {
+    if (this._autocomplete) {
+      this._autocomplete.remove();
+    }
+
+    this._autocomplete = this.componentManager.create('AutoComplete', {
+      parentContainer: this._container,
+      name: `${this.name}.autocomplete`,
+      isFilterSearch: true,
+      container: '.js-yxt-GeoLocationFilter-autocomplete',
+      originalQuery: this.query,
+      inputEl: inputSelector,
+      verticalKey: this._config.verticalKey,
+      searchParameters: this.searchParameters,
+      onSubmit: (query, filter) => this._handleSubmit(query, filter)
+    });
+  }
+
+  _handleSubmit (query, filter) {
+    this.query = query;
+    this._saveDataToStorage(query, Filter.fromResponse(filter), `${query}`);
+    this._enabled = false;
+  }
+
+  /**
+   * Toggles the static filter representing the user's location
+   * @private
+   */
+  _toggleGeoFilter () {
+    if (!navigator.geolocation) {
+      this.setState({ geoError: true });
+      return;
+    }
+
+    if (!this._enabled) {
+      this.setState({ geoLoading: true });
+      navigator.geolocation.getCurrentPosition(
+        position => {
+          const filter = this._buildFilter(position);
+          this._saveDataToStorage('', filter, 'Current Location', position);
+          this._enabled = true;
+          this.setState({});
+          this.core.storage.delete(`${StorageKeys.QUERY}.${this.name}`);
+          this.core.storage.delete(`${StorageKeys.FILTER}.${this.name}`);
+        },
+        () => this._handleGeolocationError(),
+        this._geolocationOptions
+      );
+    }
+  }
+
+  _handleGeolocationError () {
+    this.setState({ geoError: true });
+    const { enabled, message } = this._geolocationTimeoutAlert;
+    if (enabled) {
+      window.alert(message);
+    }
+  }
+
+  _removeFilterNode () {
+    this.core.storage.delete(`${StorageKeys.QUERY}.${this.name}`);
+    this.core.storage.delete(`${StorageKeys.FILTER}.${this.name}`);
+    this._enabled = false;
+    this.query = '';
+    this.core.clearStaticFilterNode(this.name);
+    this.setState();
+  }
+
+  _buildFilterNode (filter, displayValue) {
+    return FilterNodeFactory.from({
+      filter: filter,
+      metadata: {
+        displayValue: displayValue,
+        fieldName: this._config.title || this._config.label || TranslationFlagger.flag({
+          phrase: 'Location'
+        })
+      },
+      remove: () => this._removeFilterNode()
+    });
+  }
+
+  /**
+   * Saves the provided filter under this component's name
+   * @param {string} query The query to save
+   * @param {Filter} filter The filter to save
+   * @param {string} displayValue The display value for the filter
+   * @param {Object} position The position to save
+   * @private
+   */
+  _saveDataToStorage (query, filter, displayValue, position) {
+    this.core.storage.setWithPersist(`${StorageKeys.QUERY}.${this.name}`, query);
+    this.core.storage.setWithPersist(`${StorageKeys.FILTER}.${this.name}`, filter);
+    const filterNode = this._buildFilterNode(filter, displayValue);
+    this.core.setStaticFilterNodes(this.name, filterNode);
+
+    if (position) {
+      this.core.storage.set(StorageKeys.GEOLOCATION, {
+        lat: position.coords.latitude,
+        lng: position.coords.longitude,
+        radius: position.coords.accuracy
+      });
+    }
+
+    if (this._config.searchOnChange) {
+      this.core.triggerSearch(QueryTriggers.FILTER_COMPONENT);
+    }
+  }
+
+  /**
+   * Given a position, construct a Filter object
+   * @param {Postition} position The position
+   * @returns {Filter}
+   * @private
+   */
+  _buildFilter (position) {
+    const { latitude, longitude, accuracy } = position.coords;
+    const radius = Math.max(accuracy, this._config.radius * METERS_PER_MILE);
+    return Filter.position(latitude, longitude, radius);
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/ui_components_filters_rangefiltercomponent.js.html b/docs/ui_components_filters_rangefiltercomponent.js.html new file mode 100644 index 000000000..e6ffa9816 --- /dev/null +++ b/docs/ui_components_filters_rangefiltercomponent.js.html @@ -0,0 +1,289 @@ + + + + + JSDoc: Source: ui/components/filters/rangefiltercomponent.js + + + + + + + + + + +
+ +

Source: ui/components/filters/rangefiltercomponent.js

+ + + + + + +
+
+
/** @module RangeFilterComponent */
+
+import DOM from '../../dom/dom';
+import Component from '../component';
+import ComponentTypes from '../../components/componenttypes';
+import TranslationFlagger from '../../i18n/translationflagger';
+import Filter from '../../../core/models/filter';
+import FilterNodeFactory from '../../../core/filters/filternodefactory';
+import FilterMetadata from '../../../core/filters/filtermetadata';
+import Matcher from '../../../core/filters/matcher';
+import StorageKeys from '../../../core/storage/storagekeys';
+import { getPersistedRangeFilterContents } from '../../tools/filterutils';
+
+const DEFAULT_CONFIG = {
+  minPlaceholderText: TranslationFlagger.flag({
+    phrase: 'Min',
+    context: 'Minimum'
+  }),
+  maxPlaceholderText: TranslationFlagger.flag({
+    phrase: 'Max',
+    context: 'Maximum'
+  })
+};
+
+export default class RangeFilterComponent extends Component {
+  constructor (config = {}, systemConfig = {}) {
+    super({ ...DEFAULT_CONFIG, ...config }, systemConfig);
+
+    /**
+     * The field to filter on
+     * @type {string}
+     * @private
+     */
+    this._field = config.field;
+
+    /**
+     * The callback function to call when the filter value changes
+     * @type {function}
+     * @private
+     */
+    this._onChange = config.onChange || function () {};
+
+    /**
+     * If true, stores the filter to storage on each change
+     * @type {boolean}
+     * @private
+     */
+    this._storeOnChange = config.storeOnChange === undefined ? true : config.storeOnChange;
+
+    /**
+     * The title to display for the range control
+     * @type {string}
+     * @private
+     */
+    this._title = config.title;
+
+    /**
+     * The optional label to display for the min input
+     * @type {string}
+     * @private
+     */
+    this._minLabel = config.minLabel || null;
+
+    /**
+     * The optional label to display for the max input
+     * @type {string}
+     * @private
+     */
+    this._maxLabel = config.maxLabel || null;
+
+    this.seedFromPersistedFilter();
+
+    this.core.storage.registerListener({
+      storageKey: StorageKeys.HISTORY_POP_STATE,
+      eventType: 'update',
+      callback: () => {
+        this.seedFromPersistedFilter();
+        this.setState();
+      }
+    });
+  }
+
+  /**
+   * Reseeds the component state from the PERSISTED_FILTER in storage.
+   * If there is an active filter, store it in core.
+   */
+  seedFromPersistedFilter () {
+    if (this.core.storage.has(StorageKeys.PERSISTED_FILTER)) {
+      const persistedFilter = this.core.storage.get(StorageKeys.PERSISTED_FILTER);
+      const persistedFilterContents = getPersistedRangeFilterContents(persistedFilter, this._field);
+      const {
+        [Matcher.GreaterThanOrEqualTo]: minVal,
+        [Matcher.LessThanOrEqualTo]: maxVal
+      } = persistedFilterContents;
+      this._range = {
+        min: minVal,
+        max: maxVal
+      };
+    } else {
+      this._range = {
+        min: [this._config.initialMin, 0].find(v => v !== undefined),
+        max: [this._config.initialMax, 10].find(v => v !== undefined)
+      };
+    }
+
+    if (this._range.min != null || this._range.max != null) {
+      const filterNode = this.getFilterNode();
+      this.core.setStaticFilterNodes(this.name, filterNode);
+    }
+  }
+
+  static get type () {
+    return ComponentTypes.RANGE_FILTER;
+  }
+
+  static defaultTemplateName () {
+    return 'controls/range';
+  }
+
+  setState (data) {
+    super.setState(Object.assign({}, data, {
+      name: this.name,
+      title: this._title,
+      minLabel: this._minLabel,
+      maxLabel: this._maxLabel,
+      minValue: this._range.min,
+      maxValue: this._range.max
+    }));
+  }
+
+  onCreate () {
+    DOM.delegate(this._container, '.js-yext-range', 'change', event => {
+      this._updateRange(event.target.dataset.key, Number.parseInt(event.target.value));
+    });
+  }
+
+  setMin (value) {
+    this._updateRange('min', value);
+  }
+
+  setMax (value) {
+    this._updateRange('max', value);
+  }
+
+  _removeFilterNode () {
+    this._range = {
+      min: null,
+      max: null
+    };
+    this.setState();
+    this._onChange(FilterNodeFactory.from());
+    this.core.clearStaticFilterNode(this.name);
+    this.core.storage.delete(`${this.name}.min`);
+    this.core.storage.delete(`${this.name}.max`);
+  }
+
+  /**
+   * Returns this component's filter node.
+   * This method is exposed so that components like {@link FilterBoxComponent}
+   * can access them.
+   * @returns {FilterNode}
+   */
+  getFilterNode () {
+    return FilterNodeFactory.from({
+      filter: this._buildFilter(),
+      metadata: this._buildFilterMetadata(),
+      remove: () => this._removeFilterNode()
+    });
+  }
+
+  /**
+   * Update the current range state
+   * @param {string} key The range key to update
+   * @param {number} value The new value for the key
+   */
+  _updateRange (key, value) {
+    this._range = Object.assign({}, this._range, { [key]: value });
+    this.setState();
+
+    const filterNode = this.getFilterNode();
+    if (this._storeOnChange) {
+      this.core.setStaticFilterNodes(this.name, filterNode);
+    }
+
+    this._onChange(filterNode);
+  }
+
+  /**
+   * Build the filter representation of the current state
+   * @returns {Filter}
+   */
+  _buildFilter () {
+    const { min, max } = this._range;
+    const falsyMin = !min && min !== 0;
+    const falsyMax = !max && max !== 0;
+    const _min = falsyMin ? null : parseInt(min);
+    const _max = falsyMax ? null : parseInt(max);
+    return Filter.range(this._field, _min, _max, false);
+  }
+
+  /**
+   * Helper method for creating range filter metadata
+   * @returns {FilterMetadata}
+   */
+  _buildFilterMetadata () {
+    const { min, max } = this._range;
+    const falsyMin = !min && min !== 0;
+    const falsyMax = !max && max !== 0;
+    if (falsyMin && falsyMax) {
+      return new FilterMetadata({
+        fieldName: this._title
+      });
+    }
+    // TODO add config option to range filter component for exclusive ranges.
+    // Currently can only have inclusive ranges.
+    const isExclusive = false;
+    let displayValue;
+    if (falsyMax) {
+      displayValue = isExclusive
+        ? `> ${min}`
+        : `≥ ${min}`;
+    } else if (falsyMin) {
+      displayValue = isExclusive
+        ? `< ${max}`
+        : `≤ ${max}`;
+    } else if (min === max) {
+      displayValue = isExclusive ? '' : min;
+    } else {
+      displayValue = isExclusive
+        ? `> ${min}, < ${max}`
+        : `${min} - ${max}`;
+    }
+    return new FilterMetadata({
+      fieldName: this._title,
+      displayValue: displayValue
+    });
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/ui_components_filters_sortoptionscomponent.js.html b/docs/ui_components_filters_sortoptionscomponent.js.html new file mode 100644 index 000000000..ba51bf01f --- /dev/null +++ b/docs/ui_components_filters_sortoptionscomponent.js.html @@ -0,0 +1,384 @@ + + + + + JSDoc: Source: ui/components/filters/sortoptionscomponent.js + + + + + + + + + + +
+ +

Source: ui/components/filters/sortoptionscomponent.js

+ + + + + + +
+
+
/** @module SortOptionsComponent */
+
+import Component from '../component';
+import { AnswersBasicError } from '../../../core/errors/errors';
+import DOM from '../../dom/dom';
+import StorageKeys from '../../../core/storage/storagekeys';
+import ResultsContext from '../../../core/storage/resultscontext';
+import SearchStates from '../../../core/storage/searchstates';
+import ComponentTypes from '../../components/componenttypes';
+import TranslationFlagger from '../../i18n/translationflagger';
+import QueryTriggers from '../../../core/models/querytriggers';
+
+/**
+ * Renders configuration options for sorting Vertical Results.
+ * TODO: how to deal with multiple instances of this component (and filters in general),
+ * ideally "identical" filters/sorts would be synced up.
+ */
+export default class SortOptionsComponent extends Component {
+  constructor (config = {}, systemConfig = {}) {
+    super(assignDefaults(config), systemConfig);
+    this.options = this._config.options;
+    this.selectedOptionIndex = this.getPersistedSelectedOptionIndex();
+    this.options[this.selectedOptionIndex].isSelected = true;
+    this.hideExcessOptions = this._config.showMore && this.selectedOptionIndex < this._config.showMoreLimit;
+    this.searchOnChangeIsEnabled = this._config.searchOnChange;
+    this.showResetIsEnabled = this._config.showReset;
+    this.showReset = this.showResetIsEnabled && this.selectedOptionIndex !== 0;
+    this.isNoResults = false;
+
+    /**
+     * This component should only render if there are search results, so it should listen
+     * to updates to vertical results and handle them accordingly.
+     */
+    this.core.storage.registerListener({
+      eventType: 'update',
+      storageKey: StorageKeys.VERTICAL_RESULTS,
+      callback: verticalResults => {
+        const isSearchComplete = verticalResults.searchState === SearchStates.SEARCH_COMPLETE;
+
+        if (isSearchComplete) {
+          const isNoResults = verticalResults.resultsContext === ResultsContext.NO_RESULTS;
+          this.handleVerticalResultsUpdate(isNoResults);
+        }
+      }
+    });
+
+    this.core.storage.registerListener({
+      eventType: 'update',
+      storageKey: StorageKeys.HISTORY_POP_STATE,
+      callback: () => {
+        const persistedOptionIndex = this.getPersistedSelectedOptionIndex();
+        this._updateSelectedOption(persistedOptionIndex);
+        this.setState();
+      }
+    });
+  }
+
+  /**
+   * Returns the option index matching the persisted sortBys, if one exists.
+   *
+   * @returns {number|undefined}
+   */
+  getPersistedSelectedOptionIndex () {
+    const persistedSortBys = this.core.storage.get(StorageKeys.SORT_BYS) || [];
+    const persistedIndex = this._config.options.findIndex(option => {
+      return persistedSortBys.find(persistedOption =>
+        persistedOption.direction === option.direction &&
+        persistedOption.type === option.type &&
+        persistedOption.field === option.field
+      );
+    });
+    return persistedIndex === -1 ? 0 : persistedIndex;
+  }
+
+  /**
+   * Handle updates to vertical results and trigger a re-render if necessary
+   *
+   * @param {boolean} isNoResults
+   */
+  handleVerticalResultsUpdate (isNoResults) {
+    const wasNoResults = this.isNoResults;
+    this.isNoResults = isNoResults;
+
+    // Call setState (and therefore trigger a re-render) if the presence of search
+    // results has changed. By not always re-rendering, we maintain focus on the selected
+    // selected sort option
+    if (isNoResults !== wasNoResults) {
+      this.setState();
+    }
+  }
+
+  setState (data = {}) {
+    let options = this.options;
+    if (this.hideExcessOptions) {
+      options = this.options.slice(0, this._config.showMoreLimit);
+    }
+    super.setState(Object.assign({}, data, {
+      options,
+      hideExcessOptions: this.hideExcessOptions,
+      name: this.name,
+      showReset: this.showReset,
+      isNoResults: this.isNoResults
+    }));
+  }
+
+  onMount () {
+    // Handle radio button selections
+    const containerEl = DOM.query(this._container, '.yxt-SortOptions-fieldSet');
+    containerEl && DOM.on(
+      containerEl,
+      'change',
+      evt => this.handleOptionSelection(parseInt(evt.target.value))
+    );
+
+    // Register more/less button
+    if (this._config.showMore) {
+      const toggleEl = DOM.query(this._container, '.yxt-SortOptions-showToggle');
+      toggleEl && DOM.on(
+        toggleEl,
+        'click', () => {
+          this.hideExcessOptions = !this.hideExcessOptions;
+          this.setState();
+        }
+      );
+    }
+
+    // Register show reset button
+    if (this.showResetIsEnabled) {
+      const resetEl = DOM.query(this._container, '.yxt-SortOptions-reset');
+      resetEl && DOM.on(
+        resetEl,
+        'click',
+        () => {
+          this.handleOptionSelection(0);
+          this.setState();
+        }
+      );
+    }
+
+    // Register apply button
+    if (!this.searchOnChangeIsEnabled) {
+      const applyEl = DOM.query(this._container, '.yxt-SortOptions-apply');
+      applyEl && DOM.on(
+        applyEl,
+        'click',
+        () => this._sortResults()
+      );
+    }
+  }
+
+  handleOptionSelection (selectedOptionIndex) {
+    this._updateSelectedOption(selectedOptionIndex);
+    this._updateCheckedAttributes();
+
+    if (this.showResetIsEnabled) {
+      this.showReset = (selectedOptionIndex !== 0);
+      this._showOrHideResetButton();
+    }
+
+    if (this.searchOnChangeIsEnabled) {
+      this._sortResults();
+    }
+  }
+
+  _updateSelectedOption (optionIndex) {
+    this.options[this.selectedOptionIndex].isSelected = false;
+    this.options[optionIndex].isSelected = true;
+    this.selectedOptionIndex = optionIndex;
+  }
+
+  /**
+   * Set the 'checked' attribute for the selected option and remove it for all others
+   */
+  _updateCheckedAttributes () {
+    this.options.forEach((option, optionIndex) => {
+      const optionId = `#yxt-SortOptions-option_SortOptions_${optionIndex}`;
+      const optionEl = DOM.query(this._container, optionId);
+
+      if (this.selectedOptionIndex === optionIndex) {
+        optionEl && optionEl.setAttribute('checked', '');
+      } else {
+        optionEl && optionEl.removeAttribute('checked', '');
+      }
+    });
+  }
+
+  /**
+   * Show or hide the reset button based on this.showReset
+   */
+  _showOrHideResetButton () {
+    const resetEl = DOM.query(this._container, '.yxt-SortOptions-reset');
+
+    if (this.showReset) {
+      resetEl.classList.remove('js-hidden');
+    } else if (!resetEl.classList.contains('js-hidden')) {
+      resetEl.classList.add('js-hidden');
+    }
+  }
+
+  _sortResults () {
+    const optionIndex = this.selectedOptionIndex;
+    const option = this.options[optionIndex];
+
+    // searchOnChange really means sort on change here, just that the sort is done through a search,
+    // This was done to have a consistent option name between filters.
+    if (this._config.storeOnChange && optionIndex === 0) {
+      this.core.clearSortBys();
+    } else if (this._config.storeOnChange) {
+      this.core.setSortBys(option);
+    }
+    this._search();
+    this._config.onChange(option);
+  }
+
+  /**
+   * Trigger a search with all filters in storage
+   */
+  _search () {
+    this.core.triggerSearch(QueryTriggers.FILTER_COMPONENT);
+  }
+
+  static get type () {
+    return ComponentTypes.SORT_OPTIONS;
+  }
+
+  static defaultTemplateName () {
+    return 'controls/sortoptions';
+  }
+}
+
+function assignDefaults (config) {
+  const updatedConfig = Object.assign({}, config);
+
+  // Optional, The label used for the “default” sort (aka the sort specified by the experience config").
+  updatedConfig.defaultSortLabel = config.defaultSortLabel || TranslationFlagger.flag({
+    phrase: 'Best Match',
+    context: 'Best match (i.e. most relevant), describing results'
+  });
+
+  // Array of search options, where an option has type, label, and if is type FIELD also a label and direction
+  if (!config.options) {
+    throw new AnswersBasicError('config.options are required', 'SortOptions');
+  }
+  const OPTION_TYPES = ['FIELD', 'RELEVANCE', 'ENTITY_DISTANCE'];
+  if (!Array.isArray(config.options)) {
+    throw new AnswersBasicError('options must be an array of objects', 'SortOptions');
+  }
+  updatedConfig.options = config.options.map(option => {
+    if (!option.label || !option.type) {
+      throw new AnswersBasicError(`option.label and option.type are required option ${option}`, 'SortOptions');
+    }
+    const newOption = { isSelected: false };
+    newOption.label = option.label;
+    newOption.type = option.type;
+    const isField = OPTION_TYPES.indexOf(newOption.type) === 0;
+    if (isField && option.field && option.direction) {
+      newOption.field = option.field;
+      newOption.direction = option.direction;
+    } else if (isField) {
+      throw new AnswersBasicError(`option.field and option.direction are required for option: ${option}`, 'SortOptions');
+    }
+    return newOption;
+  });
+  // Add default option to the front of the options array
+  updatedConfig.options.unshift({
+    label: updatedConfig.defaultSortLabel,
+    isSelected: false
+  });
+
+  // Optional, the selector used for options in the template
+  updatedConfig.optionSelector = config.optionSelector || 'yxt-SortOptions-optionSelector';
+
+  // Optional, if true, triggers a search on each change to a filter,
+  // if false the component also renders an apply button, defaults to false
+  updatedConfig.searchOnChange = config.searchOnChange === undefined ? true : config.searchOnChange;
+
+  // Optional, show a reset button. Clicking it will always return the user to the default sorting option.
+  updatedConfig.showReset = config.showReset || false;
+
+  // Optional, the label to use for the reset button
+  updatedConfig.resetLabel = config.resetLabel || TranslationFlagger.flag({
+    phrase: 'reset',
+    context: 'Button label, deselects one or more options'
+  });
+
+  // Optional, the max number of filter options to show before collapsing extras
+  updatedConfig.showMoreLimit = config.showMoreLimit || 5;
+
+  // Optional, allow collapsing excess sort options after a limit
+  updatedConfig.showMore = config.showMore === undefined ? true : config.showMore;
+  updatedConfig.showMore = updatedConfig.showMore && (updatedConfig.options.length > updatedConfig.showMoreLimit);
+
+  // Optional, the label to show for displaying more options
+  updatedConfig.showMoreLabel = config.showMoreLabel || TranslationFlagger.flag({
+    phrase: 'Show more',
+    context: 'Displays more options'
+  });
+
+  // Optional, the label to show for displaying less options
+  updatedConfig.showLessLabel = config.showLessLabel || TranslationFlagger.flag({
+    phrase: 'Show less',
+    context: 'Displays less options'
+  });
+
+  // Optional, the callback function to call when changed
+  updatedConfig.onChange = config.onChange || function () {};
+
+  // Optional, Top title for the sorting component
+  updatedConfig.label = config.label || TranslationFlagger.flag({
+    phrase: 'Sorting',
+    context: 'Title for a group of controls that sort results'
+  });
+
+  // Optional, when true component does not update storage
+  // possibly delegating that to a higher-order/composite component
+  updatedConfig.storeOnChange = config.storeOnChange === undefined ? true : config.storeOnChange;
+
+  updatedConfig.applyLabel = config.applyLabel || TranslationFlagger.flag({
+    phrase: 'Apply',
+    context: 'Button label, effectuates changes'
+  });
+
+  updatedConfig.verticalKey = config.verticalKey ||
+    ANSWERS.core.storage.get(StorageKeys.SEARCH_CONFIG).verticalKey;
+  if (!updatedConfig.verticalKey) {
+    throw new AnswersBasicError('vertical key is required', 'SortOptions');
+  }
+
+  // note: showExpand and showNumberApplied explicitly not included, on the grounds that
+  // sorting should always be exposed to the user if added.
+
+  return updatedConfig;
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/ui_components_icons_iconcomponent.js.html b/docs/ui_components_icons_iconcomponent.js.html new file mode 100644 index 000000000..589f36534 --- /dev/null +++ b/docs/ui_components_icons_iconcomponent.js.html @@ -0,0 +1,127 @@ + + + + + JSDoc: Source: ui/components/icons/iconcomponent.js + + + + + + + + + + +
+ +

Source: ui/components/icons/iconcomponent.js

+ + + + + + +
+
+
/** @module IconComponent */
+
+import Component from '../component';
+
+export default class IconComponent extends Component {
+  /**
+   * IconComponent
+   * @param opts
+   * @param opts.iconName {string}
+   * @param opts.iconUrl {string}
+   */
+  constructor (opts = {}, systemOpts = {}) {
+    super(opts, systemOpts);
+
+    /**
+     * name of an icon from the default icon set
+     * @type {string}
+     */
+    this.iconName = opts.iconName || 'default';
+
+    /**
+     * the url to a custom image icon
+     * @type {null}
+     */
+    this.iconUrl = opts.iconUrl || null;
+
+    /**
+     * An additional string to append to the icon's css class. Multiple
+     * classes should be space delimited.
+     */
+    this.classNames = opts.classNames || null;
+
+    /**
+     * A unique id to pass to the icon.
+     * @type {Object}
+     */
+    this.complexContentsParams = opts.complexContentsParams || {};
+  }
+
+  static get type () {
+    return 'IconComponent';
+  }
+
+  /**
+   * The template to render
+   * @returns {string}
+   * @override
+   */
+  static defaultTemplateName (config) {
+    return 'icons/icon';
+  }
+
+  /**
+   * allowing duplicates
+   * @returns {boolean}
+   * @override
+   */
+  static areDuplicateNamesAllowed () {
+    return true;
+  }
+
+  /**
+   * overrides default functionality to provide name and markup
+   * @param data
+   * @returns {IconComponent}
+   */
+  setState (data) {
+    return super.setState(Object.assign(data, {
+      iconUrl: this.iconUrl,
+      iconName: this.iconName,
+      name: this.iconName ? this.iconName : 'custom',
+      classNames: this.classNames,
+      complexContentsParams: this.complexContentsParams
+    }));
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/ui_components_map_mapcomponent.js.html b/docs/ui_components_map_mapcomponent.js.html index 97327930a..a3b13892d 100644 --- a/docs/ui_components_map_mapcomponent.js.html +++ b/docs/ui_components_map_mapcomponent.js.html @@ -34,6 +34,7 @@

Source: ui/components/map/mapcomponent.js

import MapBoxMapProvider from './providers/mapboxmapprovider'; import StorageKeys from '../../../core/storage/storagekeys'; +import ResultsContext from '../../../core/storage/resultscontext'; const ProviderTypes = { 'google': GoogleMapProvider, @@ -41,8 +42,8 @@

Source: ui/components/map/mapcomponent.js

}; export default class MapComponent extends Component { - constructor (opts = {}) { - super(opts); + constructor (opts = {}, systemOpts = {}) { + super(opts, systemOpts); /** * Bind this component to listen to the storage based on this key @@ -50,9 +51,14 @@

Source: ui/components/map/mapcomponent.js

this.moduleId = StorageKeys.VERTICAL_RESULTS; /** - * The default template to use to render this component + * Configuration for the behavior when there are no vertical results. */ - this._templateName = 'results/map'; + this._noResults = { + displayAllResults: false, + visible: undefined, + template: '', + ...(opts.noResults || this.core.storage.get(StorageKeys.NO_RESULTS_CONFIG)) + }; /** * An aliased used to determine the type of map provider to use @@ -63,12 +69,6 @@

Source: ui/components/map/mapcomponent.js

throw new Error('MapComponent: Invalid Map Provider; must be `google` or `mapBox`'); } - /** - * Internal indication of whether or not to use a static or dynamic map - * @type {boolean} - */ - this._isStatic = opts.isStatic || false; - /** * A reference to an instance of the {MapProvider} that's constructed * @type {MapProvider} @@ -80,31 +80,34 @@

Source: ui/components/map/mapcomponent.js

return 'Map'; } + /** + * The template to render + * @returns {string} + * @override + */ + static defaultTemplateName (config) { + return 'results/map'; + } + // TODO(billy) Make ProviderTypes a factory class getProviderInstance (type) { - return new ProviderTypes[type.toLowerCase()](this._opts); + const _config = { + locale: this.core.storage.get(StorageKeys.LOCALE), + ...this._config, + noResults: this._noResults + }; + + return new ProviderTypes[type.toLowerCase()](_config); } onCreate () { this._map = this.getProviderInstance(this._mapProvider); - let mapData = this.getState('map'); - if (mapData === undefined && this._isStatic) { - return this; - } - - if (this._isStatic) { - // TODO(billy) The existing template should just take in the map `imgURL` as data - // Instead of overriding the template like so, but NBD for now. - this.setTemplate(this._map.generateStatic(mapData)); - return this; - } - this._map.loadJS(); } onMount () { this._map.onLoaded(() => { - this._map.init(this._container, this.getState('map')); + this._map.init(this._container, this.getState('map'), this.getState('resultsContext')); }); } @@ -113,6 +116,12 @@

Source: ui/components/map/mapcomponent.js

return this; } + if (data.resultsContext === ResultsContext.NO_RESULTS && !this._noResults.displayAllResults) { + data = { + resultsContext: data.resultsContext + }; + } + return super.setState(data, val); } } @@ -126,13 +135,13 @@

Source: ui/components/map/mapcomponent.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/ui_components_map_providers_googlemapprovider.js.html b/docs/ui_components_map_providers_googlemapprovider.js.html index 28ae94790..be0f4eb0e 100644 --- a/docs/ui_components_map_providers_googlemapprovider.js.html +++ b/docs/ui_components_map_providers_googlemapprovider.js.html @@ -42,46 +42,79 @@

Source: ui/components/map/providers/googlemapprovider.js< constructor (opts) { super(opts); + // normalize because google's zoom is effectively 1 unit of difference away from mapbox zoom + this._zoomOffset = 1; + this._zoom += this._zoomOffset; this._clientId = opts.clientId; this._signature = opts.signature; if (!this.hasValidClientCredentials() && !this._apiKey) { throw new Error('GoogleMapsProvider: Missing `apiKey` or {`clientId`, `signature`}'); } + + /** + * Language of the map. + * @type {string} + */ + this._language = this.getLanguage(this._locale); } - loadJS (onLoad) { - if (DOM.query('#yext-map-js')) { - if (typeof onLoad === 'function') { - onLoad(); + /** + * Google Maps supports some language codes that are longer than two characters. If the + * locale matches one of these edge cases, use it. Otherwise, fallback on the first two + * characters of the locale. + * @param {string} localeStr Unicode locale + */ + getLanguage (localeStr) { + const googleMapsCustomLanguages = + ['zh-CN', 'zn-HK', 'zh-TW', 'en-AU', 'en-GB', 'fr-CA', 'pt-BR', 'pt-PT', 'es-419']; + const locale = localeStr.replace('_', '-'); + + if (googleMapsCustomLanguages.includes(locale)) { + return locale; + } + + const language = locale.substring(0, 2); + return language; + } + + loadJS () { + const self = this; + const onLoad = function () { + if (typeof self._onLoaded === 'function') { + self._onLoaded(); } + }; + + if (typeof google !== 'undefined') { + self._isLoaded = true; + onLoad(); return; } - let script = DOM.createEl('script', { + let script = DOM.query('#yext-map-js'); + if (script) { + const onLoadFunc = script.onload; + script.onload = function () { + onLoadFunc(); + onLoad(); + }; + return; + } + + script = DOM.createEl('script', { id: 'yext-map-js', onload: () => { - this._isLoaded = true; - this._onLoaded(); + self._isLoaded = true; + onLoad(); }, async: true, - src: `//maps.googleapis.com/maps/api/js?${this.generateCredentials()}` + src: `https://maps.googleapis.com/maps/api/js?${self.generateCredentials()}&language=${self._language}` }); DOM.append('body', script); } - generateStatic (mapData) { - let googleMapMarkerConfigs = GoogleMapMarkerConfig.from( - mapData.mapMarkers, - this._pinConfig - ); - - let encodedMarkers = GoogleMapMarkerConfig.serialize(googleMapMarkerConfigs); - return ` - <img src="//maps.googleapis.com/maps/api/staticmap?${encodedMarkers}&size=${this._width}x${this._height}&${this.generateCredentials()}">`; - } - generateCredentials () { if (this.hasValidClientCredentials()) { return `client=${this._clientId}`; @@ -91,12 +124,11 @@

Source: ui/components/map/providers/googlemapprovider.js< } hasValidClientCredentials () { - // Signature is only required if map is static - return this._clientId && (this._signature || !this._isStatic); + return this._clientId; } - init (el, mapData) { - if (!mapData || mapData.mapMarkers.length <= 0) { + init (el, mapData, resultsContext) { + if (MapProvider.shouldHideMap(mapData, resultsContext, this._showEmptyMap, this._noResults.visible)) { this._map = null; return this; } @@ -105,30 +137,48 @@

Source: ui/components/map/providers/googlemapprovider.js< // Only here for demo purposes, so we'll fix later. setTimeout(() => { let container = DOM.query(el); - DOM.css(container, { - width: this._width, - height: this._height - }); - this.map = new google.maps.Map(container, { - zoom: this._zoom + zoom: this._zoom, + center: this.getCenterMarker(mapData) }); // Apply our search data to our GoogleMap - let bounds = new google.maps.LatLngBounds(); - let googleMapMarkerConfigs = GoogleMapMarkerConfig.from( - mapData.mapMarkers, - this._pinConfig, - this.map); - - for (let i = 0; i < googleMapMarkerConfigs.length; i++) { - let marker = new google.maps.Marker(googleMapMarkerConfigs[i]); - bounds.extend(marker.position); + if (mapData && mapData.mapMarkers.length) { + const collapsedMarkers = this._collapsePins + ? this._collapseMarkers(mapData.mapMarkers) + : mapData.mapMarkers; + let googleMapMarkerConfigs = GoogleMapMarkerConfig.from( + collapsedMarkers, + this._pinConfig, + this.map); + + let bounds = new google.maps.LatLngBounds(); + for (let i = 0; i < googleMapMarkerConfigs.length; i++) { + let marker = new google.maps.Marker(googleMapMarkerConfigs[i]); + if (this._onPinClick) { + marker.addListener('click', () => this._onPinClick(collapsedMarkers[i].item)); + } + if (this._onPinMouseOver) { + marker.addListener('mouseover', () => this._onPinMouseOver(collapsedMarkers[i].item)); + } + if (this._onPinMouseOut) { + marker.addListener('mouseout', () => this._onPinMouseOut(collapsedMarkers[i].item)); + } + bounds.extend(marker.position); + } + + if (googleMapMarkerConfigs.length >= 2) { + this.map.fitBounds(bounds); + } } - - this.map.fitBounds(bounds); }, 100); } + + getCenterMarker (mapData) { + return mapData && mapData.mapCenter && mapData.mapCenter.longitude && mapData.mapCenter.latitude + ? { lng: mapData.mapCenter.longitude, lat: mapData.mapCenter.latitude } + : { lng: this._defaultPosition.lng, lat: this._defaultPosition.lat }; + } } // TODO(billy) Move to own class @@ -183,9 +233,9 @@

Source: ui/components/map/providers/googlemapprovider.js< /** * Converts the storage data model of markers into GoogleAPIMarker - * @param {GoogleMap} A reference to the google map to apply the marker to * @param {object[]} markers The data of the marker - * @param {Object} pinConfig The configuration to apply to the marker + * @param {(Object|function)} pinConfig The configuration to apply to the marker + * @param {GoogleMap} map reference to the google map to apply the marker to * @returns {GoogleMapMarkerConfig[]} */ static from (markers, pinConfig, map) { @@ -212,7 +262,7 @@

Source: ui/components/map/providers/googlemapprovider.js< } if (pinConfigObj.scaledSize) { - icon.scaledSize = google.maps.Size(pinConfigObj.scaledSize.w, pinConfigObj.scaledSize.h); + icon.scaledSize = new google.maps.Size(pinConfigObj.scaledSize.w, pinConfigObj.scaledSize.h); } if (pinConfigObj.url) { @@ -262,13 +312,13 @@

Source: ui/components/map/providers/googlemapprovider.js<
- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/ui_components_map_providers_mapboxmapprovider.js.html b/docs/ui_components_map_providers_mapboxmapprovider.js.html index 689c9a93e..608b5ce14 100644 --- a/docs/ui_components_map_providers_mapboxmapprovider.js.html +++ b/docs/ui_components_map_providers_mapboxmapprovider.js.html @@ -28,6 +28,8 @@

Source: ui/components/map/providers/mapboxmapprovider.js<
/** @module MapBoxMapProvider */
 
+import MapboxLanguage from '@mapbox/mapbox-gl-language';
+
 import MapProvider from './mapprovider';
 import DOM from '../../../dom/dom';
 
@@ -39,6 +41,16 @@ 

Source: ui/components/map/providers/mapboxmapprovider.js< * @extends MapProvider */ export default class MapBoxMapProvider extends MapProvider { + constructor (opts = {}, systemOpts = {}) { + super(opts, systemOpts); + + /** + * Language of the map. + * @type {string} + */ + this._language = this._locale.substring(0, 2); + } + /** * Load the external JS Library * @param {function} onLoad An optional callback to invoke once the JS is loaded. @@ -50,7 +62,13 @@

Source: ui/components/map/providers/mapboxmapprovider.js< this._isLoaded = true; mapboxgl.accessToken = this._apiKey; - onLoad(); + if (typeof onLoad === 'function') { + onLoad(); + } + + if (typeof this._onLoaded === 'function') { + this._onLoaded(); + } }, async: true, src: 'https://api.mapbox.com/mapbox-gl-js/v0.44.1/mapbox-gl.js' @@ -66,54 +84,65 @@

Source: ui/components/map/providers/mapboxmapprovider.js< DOM.append('body', script); } - generateStatic (mapData) { - let mapboxMapMarkerConfigs = MapBoxMarkerConfig.from( - mapData.mapMarkers, - this._pinConfig - ); - - let center = mapData.mapCenter; - let width = this._width || 600; - let height = this._height || 200; - let zoom = this._zoom || 9; - - let encodedMarkers = MapBoxMarkerConfig.serialize(mapboxMapMarkerConfigs); - return `<img src="https://api.mapbox.com/styles/v1/mapbox/streets-v11/static/${encodedMarkers}/${center.longitude},${center.latitude},${zoom}/auto/${width}x${height}?access_token=${this._apiKey}">`; - } - - init (el, mapData) { - if (!mapData || mapData.mapMarkers.length <= 0) { + init (el, mapData, resultsContext) { + if (MapProvider.shouldHideMap(mapData, resultsContext, this._showEmptyMap, this._noResults.visible)) { this._map = null; return this; } let container = DOM.query(el); - DOM.css(container, { - width: this._width, - height: this._height - }); - this._map = new mapboxgl.Map({ container: container, zoom: this._zoom, style: 'mapbox://styles/mapbox/streets-v9', - center: [mapData.mapCenter.longitude, mapData.mapCenter.latitude] + center: this.getCenterMarker(mapData) }); - const mapboxMapMarkerConfigs = MapBoxMarkerConfig.from( - mapData.mapMarkers, - this._pinConfig, - this._map); - - for (let i = 0; i < mapboxMapMarkerConfigs.length; i++) { - let wrapper = mapboxMapMarkerConfigs[i].wrapper; - let coords = new mapboxgl.LngLat( - mapboxMapMarkerConfigs[i].position.longitude, - mapboxMapMarkerConfigs[i].position.latitude); - let marker = new mapboxgl.Marker(wrapper).setLngtLat(coords); - marker.addTo(this._map); + this._map.addControl(new MapboxLanguage({ + defaultLanguage: this._language + })); + + if (mapData && mapData.mapMarkers.length) { + const collapsedMarkers = this._collapsePins + ? this._collapseMarkers(mapData.mapMarkers) + : mapData.mapMarkers; + const mapboxMapMarkerConfigs = MapBoxMarkerConfig.from( + collapsedMarkers, + this._pinConfig, + this._map); + + const bounds = new mapboxgl.LngLatBounds(); + for (let i = 0; i < mapboxMapMarkerConfigs.length; i++) { + let wrapper = mapboxMapMarkerConfigs[i].wrapper; + let coords = new mapboxgl.LngLat( + mapboxMapMarkerConfigs[i].position.longitude, + mapboxMapMarkerConfigs[i].position.latitude); + let marker = new mapboxgl.Marker(wrapper).setLngLat(coords); + bounds.extend(marker.getLngLat()); + marker.addTo(this._map); + if (this._onPinClick) { + marker.getElement().addEventListener('click', () => this._onPinClick(collapsedMarkers[i].item)); + } + if (this._onPinMouseOver) { + marker.getElement().addEventListener('mouseover', () => + this._onPinMouseOver(collapsedMarkers[i].item)); + } + if (this._onPinMouseOut) { + marker.getElement().addEventListener('mouseout', () => + this._onPinMouseOut(collapsedMarkers[i].item)); + } + } + if (mapboxMapMarkerConfigs.length >= 2) { + this._map.fitBounds(bounds, { padding: 50 }); + } } } + + getCenterMarker (mapData) { + return mapData && mapData.mapCenter && mapData.mapCenter.longitude && mapData.mapCenter.latitude + ? [mapData.mapCenter.longitude, mapData.mapCenter.latitude] + : { lng: this._defaultPosition.lng, lat: this._defaultPosition.lat }; + } } export class MapBoxMarkerConfig { @@ -144,6 +173,12 @@

Source: ui/components/map/providers/mapboxmapprovider.js< * @type {string} */ this.label = opts.label || undefined; + + /** + * The url of the pin for the static map + * @type {string} + */ + this.staticMapPin = opts.staticMapPin || undefined; } /** @@ -154,14 +189,18 @@

Source: ui/components/map/providers/mapboxmapprovider.js< static serialize (mapboxMapMarkerConfigs) { let serializedMarkers = []; mapboxMapMarkerConfigs.forEach((marker) => { - serializedMarkers.push(`pin-s-${marker.label}(${marker.position.longitude},${marker.position.latitude})`); + if (marker.staticMapPin) { + serializedMarkers.push(`url-${marker.staticMapPin}(${marker.position.longitude},${marker.position.latitude})`); + } else { + serializedMarkers.push(`pin-s-${marker.label}(${marker.position.longitude},${marker.position.latitude})`); + } }); return serializedMarkers.join(','); } /** - * Converts the storage data model of markers into GoogleAPIMarker - * @param {MapBox} A reference to the google map to apply the marker to + * Converts the storage data model of markers into MapBoxMarkerConfig + * @param {MapBox} A reference to the mapbox map to apply the marker to * @param {object[]} markers The data of the marker * @param {Object} pinConfig The configuration to apply to the marker * @returns {MapBoxMarkerConfig[]} @@ -182,7 +221,8 @@

Source: ui/components/map/providers/mapboxmapprovider.js< marker); } - let wrapper = pinConfigObj.wrapper ? pinConfigObj.wrapper : null; + const wrapper = pinConfigObj.wrapper ? pinConfigObj.wrapper : null; + const staticMapPin = pinConfigObj.staticMapPin ? pinConfigObj.staticMapPin : null; mapboxMapMarkerConfigs.push( new MapBoxMarkerConfig({ @@ -191,7 +231,9 @@

Source: ui/components/map/providers/mapboxmapprovider.js< latitude: marker.latitude, longitude: marker.longitude }, - wrapper: wrapper + wrapper: wrapper, + label: marker.label, + staticMapPin: staticMapPin }) ); }); @@ -209,13 +251,13 @@

Source: ui/components/map/providers/mapboxmapprovider.js<
- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/ui_components_map_providers_mapprovider.js.html b/docs/ui_components_map_providers_mapprovider.js.html index f22cf0779..849c7dc8c 100644 --- a/docs/ui_components_map_providers_mapprovider.js.html +++ b/docs/ui_components_map_providers_mapprovider.js.html @@ -28,38 +28,47 @@

Source: ui/components/map/providers/mapprovider.js

/** @module MapProvider */
 
+import ResultsContext from '../../../../core/storage/resultscontext';
+
 /**
  * A MapProvider is an interface that represents that should be implemented
- * in order to integrate with a Third Party Map provider for both
- * static and interactive maps. MapProviders are used by the MapComponent.
+ * in order to integrate with a Third Party Map provider for
+ * interactive maps. MapProviders are used by the MapComponent.
  *
  * Implementations should extend this interface.
  */
 export default class MapProvider {
-  constructor (opts = {}) {
+  constructor (config = {}) {
     /**
      * The API Key used for interacting with the map provider
      * @type {string}
      */
-    this._apiKey = opts.apiKey;
+    this._apiKey = config.apiKey;
 
     /**
-     * The height of the map to append to the DOM, defaults to 100%
+     * The zoom level of the map, defaults to 14
      * @type {number}
      */
-    this._height = opts.height || 200;
+    this._zoom = config.zoom || 14;
 
     /**
-     * The width of the map to append to the DOM, defaults to 100%
-     * @type {number}
+     * The default coordinates to display if there are no results returned
+     * Only used if showEmptyMap is set to true
+     * @type {Object}
      */
-    this._width = opts.width || 600;
+    this._defaultPosition = config.defaultPosition || { lat: 37.0902, lng: -95.7129 };
 
     /**
-     * The zoom level of the map, defaults to 9
-     * @type {number}
+     * Configuration for the behavior when there are no vertical results.
+     * @type {Object}
      */
-    this._zoom = opts.zoom || 9;
+    this._noResults = config.noResults || {};
+
+    /**
+     * Determines if an empty map should be shown when there are no results
+     * @type {boolean}
+     */
+    this._showEmptyMap = config.showEmptyMap || false;
 
     /**
      * A reference to the underlying map instance, created by the external lib.
@@ -73,17 +82,61 @@ 

Source: ui/components/map/providers/mapprovider.js

*/ this._isLoaded = false; + /** + * Callback to invoke when a pin is clicked. The clicked item(s) are passed to the callback + * @type {function} + */ + this._onPinClick = config.onPinClick || null; + + /** + * Callback to invoke when a pin is hovered. The hovered item is passed to the callback + * @type {function} + */ + this._onPinMouseOver = config.onPinMouseOver || null; + + /** + * Callback to invoke when a pin is no longer hovered after being hovered. + * The hovered item is passed to the callback + * @type {function} + */ + this._onPinMouseOut = config.onPinMouseOut || null; + /** * Callback to invoke once the Javascript is loaded * @type {function} */ - this._onLoaded = opts.onLoaded || function () {}; + this._onLoaded = config.onLoaded || function () {}; /** * The custom configuration override to use for the map markers * @type {Object|Function} */ - this._pinConfig = typeof opts.pin === 'function' ? opts.pin : Object.assign(MapProvider.DEFAULT_PIN_CONFIG, opts.pin); + this._pinConfig = typeof config.pin === 'function' ? config.pin : Object.assign(MapProvider.DEFAULT_PIN_CONFIG, config.pin); + + /** + * Determines whether or not to collapse pins at the same lat/lng + * @type {boolean} + */ + this._collapsePins = config.collapsePins || false; + + /** + * Locale of the map. MapComponent supplies the locale specifed by + * ANSWERS.init() by default + * @type {string} + */ + this._locale = this._getValidatedLocale(config.locale); + } + + /** + * Returns the locale if it passes validation, otherwise returns 'en' + * @param {string} locale + */ + _getValidatedLocale (locale) { + if (locale.length < 2) { + console.error(`Locale '${locale}' must include at least two characters. Falling back to 'en'`); + return 'en'; + } + return locale; } /** @@ -103,6 +156,14 @@

Source: ui/components/map/providers/mapprovider.js

}; } + static shouldHideMap (mapData, resultsContext, showEmptyMap, visibleForNoResults) { + if (resultsContext === ResultsContext.NO_RESULTS && visibleForNoResults !== undefined) { + return !visibleForNoResults; + } + const hasEmptyMap = !mapData || mapData.mapMarkers.length <= 0; + return hasEmptyMap && !showEmptyMap; + } + onLoaded (cb) { if (typeof cb !== 'function') { return; @@ -122,14 +183,40 @@

Source: ui/components/map/providers/mapprovider.js

throw new Error('Unimplemented Method: loadJS'); } - loadStatic () { - throw new Error('Unimplemented Method: loadStatic'); - } - init (mapData) { // TODO(billy) This should be based off a promise that gets created from loadJS throw new Error('Unimplemented Method: init'); } + + /** + * Given a list of markers, combine markers with the same lat/lng into a single marker + * @param {object[]} markers The markers to collapse + */ + _collapseMarkers (markers) { + const locationToItem = {}; + markers.forEach(m => { + locationToItem[`${m.latitude}${m.longitude}`] + ? locationToItem[`${m.latitude}${m.longitude}`].push(m) + : locationToItem[`${m.latitude}${m.longitude}`] = [m]; + }); + + const collapsedMarkers = []; + for (let [, markers] of Object.entries(locationToItem)) { + if (markers.length > 1) { + const collapsedMarker = { + item: markers.map(m => m.item), + label: markers.length, + latitude: markers[0].latitude, + longitude: markers[0].longitude + }; + collapsedMarkers.push(collapsedMarker); + } else { + collapsedMarkers.push(markers[0]); + } + } + + return collapsedMarkers; + } }
@@ -141,13 +228,13 @@

Source: ui/components/map/providers/mapprovider.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/ui_components_navigation_navigationcomponent.js.html b/docs/ui_components_navigation_navigationcomponent.js.html index 9e6df3297..7a11ae344 100644 --- a/docs/ui_components_navigation_navigationcomponent.js.html +++ b/docs/ui_components_navigation_navigationcomponent.js.html @@ -28,9 +28,36 @@

Source: ui/components/navigation/navigationcomponent.js
/** @module NavigationComponent */
 
+/* global Node */
+
 import Component from '../component';
 import { AnswersComponentError } from '../../../core/errors/errors';
 import StorageKeys from '../../../core/storage/storagekeys';
+import DOM from '../../dom/dom';
+import { mergeTabOrder, getDefaultTabOrder, getUrlParams } from '../../tools/taborder';
+import { filterParamsForExperienceLink, replaceUrlParams } from '../../../core/utils/urlutils.js';
+import TranslationFlagger from '../../i18n/translationflagger';
+
+/**
+ * The debounce duration for resize events
+ * @type {number}
+ */
+const RESIZE_DEBOUNCE = 100;
+
+/**
+ * The breakpoint for mobile
+ * @type {number}
+ */
+const MOBILE_BREAKPOINT = 767;
+
+/**
+ * Enum options for mobile overflow beahvior
+ * @type {Object.<string, string>}
+ */
+const MOBILE_OVERFLOW_BEHAVIOR_OPTION = {
+  COLLAPSE: 'COLLAPSE',
+  INNERSCROLL: 'INNERSCROLL'
+};
 
 /**
  * The Tab is a model that is used to power the Navigation tabs in the view.
@@ -61,7 +88,7 @@ 

Source: ui/components/navigation/navigationcomponent.jsSource: ui/components/navigation/navigationcomponent.jsSource: ui/components/navigation/navigationcomponent.jsSource: ui/components/navigation/navigationcomponent.js} * @private */ - this._templateName = 'navigation/navigation'; + this._tabsConfig = config.verticalPages || + this.core.storage.get(StorageKeys.VERTICAL_PAGES_CONFIG).get(); /** - * Unordered map of each tab, keyed by VS configId + * Unordered map of each tab, keyed by VS verticalKey * @type {Object.<String, Object>} * @private */ - this._tabs = Tab.from(config.tabs); + this._tabs = Tab.from(this._tabsConfig); /** * The order of the tabs, parsed from configuration or URL. * This gets updated based on the server results - * @type {Array.<String>} The list of VS configIds + * @type {Array.<String>} The list of VS verticalKeys + * @private + */ + this._tabOrder = getDefaultTabOrder( + this._tabsConfig, getUrlParams(this.core.storage.getCurrentStateUrlMerged())); + + /** + * Breakpoints at which navigation items move to the "more" dropdown + * @type {number[]} * @private */ - this._tabOrder = this.getDefaultTabOrder(config.tabs, this.getUrlParams()); + this._navBreakpoints = []; + + /** + * The mobile overflow behavior config + * @type {string} + */ + this._mobileOverflowBehavior = config.mobileOverflowBehavior || MOBILE_OVERFLOW_BEHAVIOR_OPTION.COLLAPSE; + + /** + * The ARIA label + * @type {string} + */ + this._ariaLabel = config.ariaLabel || TranslationFlagger.flag({ + phrase: 'Search Page Navigation', + context: 'Noun, labels the navigation for the search page' + }); + + this.checkOutsideClick = this.checkOutsideClick.bind(this); + this.checkMobileOverflowBehavior = this.checkMobileOverflowBehavior.bind(this); + + const reRender = () => { + this.setState(this.core.storage.get(StorageKeys.NAVIGATION) || {}); + }; + + this.core.storage.registerListener({ + eventType: 'update', + storageKey: StorageKeys.API_CONTEXT, + callback: reRender + }); + this.core.storage.registerListener({ + eventType: 'update', + storageKey: StorageKeys.SESSIONS_OPT_IN, + callback: reRender + }); } static get type () { @@ -148,107 +239,222 @@

Source: ui/components/navigation/navigationcomponent.js navWidth) { + this._navBreakpoints.push(mainLinksWidth); + const lastLink = mainLinks.children.item(mainLinks.children.length - 1); + if (lastLink === null) { + return; } + this._prepend(collapsedLinks, lastLink); - // Avoid duplicates if config was provided from URL - if (tabOrder.includes(tab.configId)) { - continue; + if (moreButton.classList.contains('yxt-Nav-item--more')) { + moreButton.classList.remove('yxt-Nav-item--more'); + } + } else { + if (numBreakpoints && navWidth > this._navBreakpoints[numBreakpoints - 1]) { + const firstLink = collapsedLinks.children.item(0); + if (firstLink === null) { + return; + } + mainLinks.append(firstLink); + this._navBreakpoints.pop(); + numBreakpoints--; } - // isFirst should always be the first element in the list - if (tab.isFirst) { - tabOrder.unshift(tab.configId); - } else { - tabOrder.push(tab.configId); + if (collapsedLinks.children.length === 0) { + moreButton.classList.add('yxt-Nav-item--more'); } } - return tabOrder; + this.closeMoreDropdown(); + if (mainLinksWidth > navWidth || + (numBreakpoints > 0 && navWidth > this._navBreakpoints[numBreakpoints - 1])) { + this.refitNav(); + } + } + + closeMoreDropdown () { + const collapsed = DOM.query(this._container, '.yxt-Nav-modal'); + collapsed.classList.remove('is-active'); + const moreButton = DOM.query(this._container, '.yxt-Nav-more'); + moreButton.setAttribute('aria-expanded', false); + } + + openMoreDropdown () { + const collapsed = DOM.query(this._container, '.yxt-Nav-modal'); + collapsed.classList.add('is-active'); + const moreButton = DOM.query(this._container, '.yxt-Nav-more'); + moreButton.setAttribute('aria-expanded', true); + } + + toggleMoreDropdown () { + const collapsed = DOM.query(this._container, '.yxt-Nav-modal'); + collapsed.classList.toggle('is-active'); + const moreButton = DOM.query(this._container, '.yxt-Nav-more'); + moreButton.setAttribute('aria-expanded', collapsed.classList.contains('is-active')); + } + + checkOutsideClick (e) { + if (this._closest(e.target, '.yxt-Nav-container')) { + return; + } + + this.closeMoreDropdown(); + } + + checkMobileOverflowBehavior () { + if (this._checkMobileOverflowBehaviorTimer) { + clearTimeout(this._checkMobileOverflowBehaviorTimer); + } + + this._checkMobileOverflowBehaviorTimer = setTimeout(this.setState.bind(this), RESIZE_DEBOUNCE); } /** - * mergeTabOrder merges two arrays into one - * by appending additional tabs to the end of the original array - * @param {string[]} tabOrder Tab order provided by the server - * @param {string[]} otherTabOrder Tab order provided by configuration - * @return {string[]} + * Since the server data only provides a list of + * VS verticalKeys, we need to compute and transform + * the data into the proper format for rendering. + * + * @override */ - mergeTabOrder (tabOrder, otherTabOrder) { - for (let i = 0; i < otherTabOrder.length; i++) { - const tabConfig = otherTabOrder[i]; - if (tabOrder.includes(tabConfig)) { - continue; - } + setState (data = {}) { + if (data.tabOrder !== undefined) { + this._tabOrder = mergeTabOrder(data.tabOrder, this._tabOrder, this._tabs); + } + + const params = getUrlParams(this.core.storage.getCurrentStateUrlMerged()); + params.set('tabOrder', this._tabOrder); + const context = this.core.storage.get(StorageKeys.API_CONTEXT); + if (context) { + params.set(StorageKeys.API_CONTEXT, context); + } + const referrerPageUrl = this.core.storage.get(StorageKeys.REFERRER_PAGE_URL); + if (referrerPageUrl !== undefined) { + params.set(StorageKeys.REFERRER_PAGE_URL, referrerPageUrl); + } - // isFirst should be an override to dynamic tab ordering. - if (this._tabs[tabConfig] && this._tabs[tabConfig].isFirst) { - tabOrder.unshift(tabConfig); - } else { - tabOrder.push(tabConfig); + const filteredParams = filterParamsForExperienceLink( + params, + types => this.componentManager.getComponentNamesForComponentTypes(types) + ); + + // Since the tab ordering can change based on the server data + // we need to update each tabs URL to include the order as part of their params. + // This helps with persisting state across verticals. + const tabs = []; + for (let i = 0; i < this._tabOrder.length; i++) { + const tab = this._tabs[this._tabOrder[i]]; + if (tab !== undefined) { + tab.url = replaceUrlParams(tab.baseUrl, filteredParams); + tabs.push(tab); } } - return tabOrder; + return super.setState({ + tabs: tabs, + overflowLabel: this.overflowLabel, + overflowIcon: this.overflowIcon, + showCollapse: this.shouldCollapse(), + ariaLabel: this._ariaLabel + }); } - generateTabUrl (baseUrl, params = new URLSearchParams()) { - // We want to persist the params from the existing URL to the new - // URLS we create. - params.set('tabOrder', this._tabOrder); - return baseUrl + '?' + params.toString(); + // TODO (agrow) investigate removing this + // ParentNode.prepend polyfill + // https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/prepend#Polyfill + _prepend (collapsedLinks, lastLink) { + if (!collapsedLinks.hasOwnProperty('prepend')) { + const docFrag = document.createDocumentFragment(); + const isNode = lastLink instanceof Node; + docFrag.appendChild(isNode ? lastLink : document.createTextNode(String(lastLink))); + + collapsedLinks.insertBefore(docFrag, collapsedLinks.firstChild); + return; + } + + collapsedLinks.prepend(lastLink); + } + + // TODO (agrow) investigate removing this + // Adapted from Element.closest polyfill + // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill + _closest (el, closestElSelector) { + if (!el.hasOwnProperty('closest')) { + do { + if (DOM.matches(el, closestElSelector)) return el; + el = el.parentElement || el.parentNode; + } while (el !== null && el.nodeType === 1); + return null; + } + return el.closest(closestElSelector); + } + + shouldCollapse () { + switch (this._mobileOverflowBehavior) { + case MOBILE_OVERFLOW_BEHAVIOR_OPTION.COLLAPSE: + return true; + case MOBILE_OVERFLOW_BEHAVIOR_OPTION.INNERSCROLL: + const container = DOM.query(this._container, '.yxt-Nav-container') || this._container; + const navWidth = container.offsetWidth; + return navWidth > MOBILE_BREAKPOINT; + } } }

@@ -261,13 +467,13 @@

Source: ui/components/navigation/navigationcomponent.js
- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/ui_components_questions_questionsubmissioncomponent.js.html b/docs/ui_components_questions_questionsubmissioncomponent.js.html index 803dfd400..546116773 100644 --- a/docs/ui_components_questions_questionsubmissioncomponent.js.html +++ b/docs/ui_components_questions_questionsubmissioncomponent.js.html @@ -31,99 +31,423 @@

Source: ui/components/questions/questionsubmissioncompone import Component from '../component'; import DOM from '../../dom/dom'; import StorageKeys from '../../../core/storage/storagekeys'; - +import QuestionSubmission from '../../../core/models/questionsubmission'; +import { AnswersComponentError } from '../../../core/errors/errors'; +import AnalyticsEvent from '../../../core/analytics/analyticsevent'; +import SearchStates from '../../../core/storage/searchstates'; +import TranslationFlagger from '../../i18n/translationflagger'; + +/** + * Configurable options for the component + * @type {Object} + */ +const DEFAULT_CONFIG = { + /** + * The entity identifier that the question is associated with. + * This is typically an organization object + * @type {number} + */ + 'entityId': null, + + /** + * The main CSS selector used to reference the form for the component. + * @type {string} CSS selector + */ + 'formSelector': 'form', + + /** + * An optional label to use for the e-mail address input + * @type {string} + */ + 'emailLabel': TranslationFlagger.flag({ + phrase: 'Email', + context: 'Labels the email value provided as an argument' + }), + + /** + * An optional label to use for the name input + * @type {string} + */ + 'nameLabel': TranslationFlagger.flag({ + phrase: 'Name', + context: 'Labels the name value provided as an argument' + }), + + /** + * An optional label to use for the question + * @type {string} + */ + 'questionLabel': TranslationFlagger.flag({ + phrase: 'Question', + context: 'Labels the question value provided as an argument' + }), + + /** + * An optional label to use for the Privacy Policy + * @type {string} + */ + 'privacyPolicyText': TranslationFlagger.flag({ + phrase: 'By submitting my email address, I consent to being contacted via email at the address provided.' + }), + + /** + * The label to use for the Submit button + * @type {string} + */ + 'buttonLabel': TranslationFlagger.flag({ + phrase: 'Submit', + context: 'Button label' + }), + + /** + * The title to display in the title bar + * @type {string} + */ + 'sectionTitle': TranslationFlagger.flag({ + phrase: 'Ask a Question', + context: 'Title of section' + }), + + /** + * The description to display in the title bar + * @type {string} + */ + 'teaser': TranslationFlagger.flag({ + phrase: 'Can’t find what you\'re looking for? Ask a question below.' + }), + + /** + * The name of the icon to use in the title bar + * @type {string} + */ + 'sectionTitleIconName': 'support', + + /** + * The text to display in the feedback form ahead of the Question input + * @type {string} + */ + 'description': TranslationFlagger.flag({ + phrase: 'Enter your question and contact information, and we\'ll get back to you with a response shortly.' + }), + + /** + * The placeholder text for required inputs + * @type {string} + */ + 'requiredInputPlaceholder': TranslationFlagger.flag({ + phrase: '(required)', + context: 'Indicates that entering input is mandatory' + }), + + /** + * The placeholder text for the question text area + * @type {string} + */ + 'questionInputPlaceholder': TranslationFlagger.flag({ + phrase: 'Enter your question here', + context: 'Placeholder text for input field' + }), + + /** + * The confirmation text to display after successfully submitting feedback + * @type {string} + */ + 'questionSubmissionConfirmationText': TranslationFlagger.flag({ + phrase: 'Thank you for your question!' + }), + + /** + * The default privacy policy url label + * @type {string} + */ + 'privacyPolicyUrlLabel': TranslationFlagger.flag({ + phrase: 'Learn more here.', + context: 'Labels a link' + }), + + /** + * The default privacy policy url + * @type {string} + */ + 'privacyPolicyUrl': '', + + /** + * The default privacy policy error text, shown when the user does not agree + * @type {string} + */ + 'privacyPolicyErrorText': TranslationFlagger.flag({ + phrase: '* You must agree to the privacy policy to submit a question.' + }), + + /** + * The default email format error text, shown when the user submits an invalid email + * @type {string} + */ + 'emailFormatErrorText': TranslationFlagger.flag({ + phrase: '* Please enter a valid email address.' + }), + + /** + * The default network error text, shown when there is an issue with the QA Submission + * request. + * @type {string} + */ + 'networkErrorText': TranslationFlagger.flag({ + phrase: 'We\'re sorry, an error occurred.' + }), + + /** + * Whether or not this component is expanded by default. + * @type {boolean} + */ + 'expanded': true +}; + +/** + * QuestionSubmissionComponent is a component that creates a form + * thats displayed whenever a query is run. It enables the user + * to submit questions that they cant find the answer for. + */ export default class QuestionSubmissionComponent extends Component { - constructor (opts = {}) { - super(opts); - - this.moduleId = StorageKeys.UNIVERSAL_RESULTS; - - this._templateName = 'questions/submission'; - - /** - * Question submission is based on a form as context. - * Optionally provided, otherwise defaults to native form node within container - * @type {string} CSS selector - */ - this._formEl = opts.formSelector || 'form'; + constructor (config = {}, systemConfig = {}) { + super(Object.assign({}, DEFAULT_CONFIG, config), systemConfig); /** - * The label to use for the e-mail address input - * Optionally provided, otherwise defaults to `Email Address` + * Reference to the storage model * @type {string} */ - this._emailLabel = opts.emailLabel || '*Email Address:'; + this.moduleId = StorageKeys.QUESTION_SUBMISSION; /** - * The label to use for the name input - * Optionally provided, otherwise defaults to `Name` + * Reference to the locale as set in the global config * @type {string} */ - this._nameLabel = opts.nameLabel || 'Name:'; + this.locale = this.core.storage.get(StorageKeys.LOCALE); /** - * The label to use for the Question - * Optionally provided, otherwise defaults to `What is your question?` - * @type {string} + * NOTE(billy) if this is a pattern we want to follow for configuration + * we should bake it into the core class. */ - this._questionLabel = opts.questionLabel || '*What is your question?'; + this.validateConfig(); /** - * The label to use for the Privacy Policy - * Optionally provided, otherwise defaults to `What is your question?` - * @type {string} + * The QuestionSubmission component should be rendered only once a search has completed. If the + * search results are still loading, the component should not be displayed. */ - this._privacyPolicyLabel = opts.privacyPolicyLabel || 'I agree to our Privacy Policy:'; + const onResultsUpdate = results => { + if (results.searchState !== SearchStates.SEARCH_LOADING) { + const questionText = this.core.storage.get(StorageKeys.QUERY); + this.setState(new QuestionSubmission({ + questionText: questionText, + expanded: this._config.expanded + })); + } else { + this.unMount(); + } + }; + + this.core.storage.registerListener({ + eventType: 'update', + storageKey: StorageKeys.VERTICAL_RESULTS, + callback: onResultsUpdate + }); - /** - * The label to use for the Submit button - * Optionally provided, otherwise defaults to `Submit?` - * @type {string} - */ - this._buttonLabel = opts.buttonLabel || 'Submit'; + this.core.storage.registerListener({ + eventType: 'update', + storageKey: StorageKeys.UNIVERSAL_RESULTS, + callback: onResultsUpdate + }); } - beforeMount () { - // Only mount our component if the query has been triggered at least once. - if (this.getState('hasQueried') === true) { - return true; + /** + * The template to render + * @returns {string} + * @override + */ + static defaultTemplateName (config) { + return 'questions/questionsubmission'; + } + + /** + * The public interface alias for the component + * @returns {string} + * @override + */ + static get type () { + return 'QASubmission'; + } + + /** + * validationConfig contains a bunch of rules + * that are used to validate aginst configuration provided by the user + */ + validateConfig () { + if (this._config.entityId === null || this._config.entityId === undefined) { + throw new AnswersComponentError( + '`entityId` is a required configuration option for Question Submission', + 'QuestionSubmission'); } + } - return false; + beforeMount () { + // Avoid mounting the component if theres no data + // Note, 1 because `config` is always part of the state. + return Object.keys(this.getState()).length > 1; } onMount () { - this.initSubmit(this._formEl); + let triggerEl = DOM.query(this._container, '.js-content-visibility-toggle'); + if (triggerEl !== null) { + this.bindFormToggle(triggerEl); + } + + let formEl = DOM.query(this._container, this._config.formSelector); + if (formEl === null) { + return; + } + + this.bindFormFocus(formEl); + this.bindFormSubmit(formEl); } - initSubmit (formSelector) { - this._formEl = formSelector; + /** + * bindFormFocus will wire up the DOM focus event to serverside reporting + * @param {HTMLElement} formEl + */ + bindFormFocus (formEl) { + if (this.analyticsReporter === null) { + return; + } - let form = DOM.query(this._container, this._formEl); + const questionText = DOM.query(formEl, '.js-question-text'); + DOM.on(questionText, 'focus', () => { + this.analyticsReporter.report(this.getAnalyticsEvent('QUESTION_FOCUS')); + }); + } - DOM.on(form, 'submit', (e) => { + /** + * bindFormSubmit handles submitting the question to the server, + * and submits an event to serverside reporting + * @param {HTMLElement} formEl + */ + bindFormSubmit (formEl) { + DOM.on(formEl, 'submit', (e) => { e.preventDefault(); - // TODO(billy) Serialize form - // this.core.submitQuestion(form); + this.analyticsReporter.report(this.getAnalyticsEvent('QUESTION_SUBMIT')); + + // TODO(billy) we probably want to disable the form from being submitted twice + const errors = this.validate(formEl); + const formData = this.parse(formEl); + if (Object.keys(errors).length) { + return this.setState(new QuestionSubmission(formData, errors)); + } + + this.core.submitQuestion({ + 'entityId': this._config.entityId, + 'site': 'FIRSTPARTY', + 'name': formData.name, + 'email': formData.email, + 'questionText': formData.questionText, + 'questionDescription': formData.questionDescription + }) + .catch(error => { + this.setState( + new QuestionSubmission(formData, { + 'network': 'We\'re sorry, an error occurred.' + }) + ); + throw error; + }); }); } - static get type () { - return 'QASubmission'; + /** + * bindFormToggle handles expanding and mimimizing the component's form. + * @param {HTMLElement} triggerEl + */ + bindFormToggle (triggerEl) { + DOM.on(triggerEl, 'click', (e) => { + const formData = this.getState(); + this.setState( + new QuestionSubmission({ + ...formData, + 'expanded': !formData.questionExpanded, + 'submitted': formData.questionSubmitted }, + formData.errors)); + }); + } + + /** + * Takes the form, and builds a object that represents the input names + * and text fields. + * @param {HTMLElement} formEl + * @returns {object} + */ + parse (formEl) { + const inputFields = DOM.queryAll(formEl, '.js-question-field'); + if (!inputFields || inputFields.length === 0) { + return {}; + } + + let obj = {}; + for (let i = 0; i < inputFields.length; i++) { + let val = inputFields[i].value; + if (inputFields[i].type === 'checkbox') { + val = inputFields[i].checked; + } + obj[inputFields[i].name] = val; + } + + return obj; + } + + /** + * Validates the fields for correct formatting + * @param {HTMLElement} formEl + * @returns {Object} errors object if any errors found + */ + validate (formEl) { + let errors = {}; + const fields = DOM.queryAll(formEl, '.js-question-field'); + for (let i = 0; i < fields.length; i++) { + if (!fields[i].checkValidity()) { + if (i === 0) { + // set focus state on first error + fields[i].focus(); + } + switch (fields[i].name) { + case 'email': + errors['emailError'] = true; + if (!fields[i].validity.valueMissing) { + errors['emailErrorText'] = this._config.emailFormatErrorText; + } + break; + case 'name': + errors['nameError'] = true; + break; + case 'privacyPolicy': + errors['privacyPolicyErrorText'] = this._config.privacyPolicyErrorText; + errors['privacyPolicyError'] = true; + break; + case 'questionText': + errors['questionTextError'] = true; + break; + } + } + } + return errors; } - setState (data, val) { - // Since we're binding to search results, - // and we only want to show the QA submission - return super.setState({ - hasQueried: data.sections !== undefined, - emailLabel: this._emailLabel, - nameLabel: this._nameLabel, - questionLabel: this._questionLabel, - privacyPolicyLabel: this._privacyPolicyLabel, - buttonLabel: this._buttonLabel, - question: new URLSearchParams(window.location.search.substring(1)).get('query') + /** + * Returns an options object describing the context of a reportable event + */ + getAnalyticsEvent (eventType) { + const analyticsEvent = new AnalyticsEvent(eventType); + analyticsEvent.addOptions({ + verticalConfigId: this._verticalKey, + searcher: this._verticalKey ? 'VERTICAL' : 'UNIVERSAL' }); + return analyticsEvent; } }

@@ -136,13 +460,13 @@

Source: ui/components/questions/questionsubmissioncompone
- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/ui_components_registry.js.html b/docs/ui_components_registry.js.html new file mode 100644 index 000000000..df572b659 --- /dev/null +++ b/docs/ui_components_registry.js.html @@ -0,0 +1,154 @@ + + + + + JSDoc: Source: ui/components/registry.js + + + + + + + + + + +
+ +

Source: ui/components/registry.js

+ + + + + + +
+
+
/** @module */
+
+import Component from './component';
+
+import NavigationComponent from './navigation/navigationcomponent';
+
+import SearchComponent from './search/searchcomponent';
+import FilterSearchComponent from './search/filtersearchcomponent';
+import AutoCompleteComponent from './search/autocompletecomponent';
+import SpellCheckComponent from './search/spellcheckcomponent';
+import LocationBiasComponent from './search/locationbiascomponent';
+
+import FilterBoxComponent from './filters/filterboxcomponent';
+import FilterOptionsComponent from './filters/filteroptionscomponent';
+import RangeFilterComponent from './filters/rangefiltercomponent';
+import DateRangeFilterComponent from './filters/daterangefiltercomponent';
+import FacetsComponent from './filters/facetscomponent';
+import GeoLocationComponent from './filters/geolocationcomponent';
+import SortOptionsComponent from './filters/sortoptionscomponent';
+
+import DirectAnswerComponent from './results/directanswercomponent';
+import AccordionResultsComponent from './results/accordionresultscomponent.js';
+import VerticalResultsComponent from './results/verticalresultscomponent';
+import UniversalResultsComponent from './results/universalresultscomponent';
+import PaginationComponent from './results/paginationcomponent';
+
+import CardComponent from './cards/cardcomponent';
+import StandardCardComponent from './cards/standardcardcomponent';
+import AccordionCardComponent from './cards/accordioncardcomponent';
+import LegacyCardComponent from './cards/legacycardcomponent';
+
+import AlternativeVerticalsComponent from './results/alternativeverticalscomponent';
+import MapComponent from './map/mapcomponent';
+import QuestionSubmissionComponent from './questions/questionsubmissioncomponent';
+
+import IconComponent from './icons/iconcomponent.js';
+import CTAComponent from './ctas/ctacomponent';
+import CTACollectionComponent from './ctas/ctacollectioncomponent';
+import ResultsHeaderComponent from './results/resultsheadercomponent';
+
+import VerticalResultsCountComponent from './results/verticalresultscountcomponent';
+import AppliedFiltersComponent from './results/appliedfilterscomponent';
+
+const COMPONENT_CLASS_LIST = [
+  // Core Component
+  Component,
+
+  // Navigation Components
+  NavigationComponent,
+
+  // Search Components
+  SearchComponent,
+  FilterSearchComponent,
+  AutoCompleteComponent,
+  SpellCheckComponent,
+  LocationBiasComponent,
+
+  // Filter Components
+  FilterBoxComponent,
+  FilterOptionsComponent,
+  RangeFilterComponent,
+  DateRangeFilterComponent,
+  FacetsComponent,
+  GeoLocationComponent,
+  SortOptionsComponent,
+
+  // Results Components
+  DirectAnswerComponent,
+  UniversalResultsComponent,
+  VerticalResultsComponent,
+  PaginationComponent,
+  AccordionResultsComponent,
+  MapComponent,
+  AlternativeVerticalsComponent,
+  ResultsHeaderComponent,
+
+  // Card Components
+  CardComponent,
+  StandardCardComponent,
+  AccordionCardComponent,
+  LegacyCardComponent,
+
+  // Questions Components
+  QuestionSubmissionComponent,
+
+  // Helper Components
+  IconComponent,
+  CTAComponent,
+  CTACollectionComponent,
+  VerticalResultsCountComponent,
+  AppliedFiltersComponent
+];
+
+/**
+ * The component registry is a map that contains
+ * all available component classes used for creation or extension.
+ * Each component class has a unique type, which is used as the key for the registry
+ * @type {Object.<string, Component>}
+ */
+export const COMPONENT_REGISTRY = COMPONENT_CLASS_LIST.reduce((registry, clazz) => {
+  registry[clazz.type] = clazz;
+  return registry;
+}, {});
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/ui_components_results_accordionresultscomponent.js.html b/docs/ui_components_results_accordionresultscomponent.js.html new file mode 100644 index 000000000..e97270245 --- /dev/null +++ b/docs/ui_components_results_accordionresultscomponent.js.html @@ -0,0 +1,194 @@ + + + + + JSDoc: Source: ui/components/results/accordionresultscomponent.js + + + + + + + + + + +
+ +

Source: ui/components/results/accordionresultscomponent.js

+ + + + + + +
+
+
/** @module AccordionResultsComponent */
+import VerticalResultsComponent from './verticalresultscomponent';
+import DOM from '../../dom/dom';
+import AnalyticsEvent from '../../../core/analytics/analyticsevent';
+
+export default class AccordionResultsComponent extends VerticalResultsComponent {
+  constructor (config = {}, systemConfig = {}) {
+    super(config, systemConfig);
+
+    /**
+     * base selector to use when finding DOM targets
+     * @type {string}
+     */
+    this._selectorBase = config.selectorBase || '.js-yxt-AccordionResult';
+
+    /**
+     * collapsed state class
+     * @type {string}
+     */
+    this.collapsedClass = config.collapsedClass || 'is-collapsed';
+
+    /**
+     * vertical config id is required for analytics
+     * @type {string|null}
+     */
+    this.verticalConfigId = config.verticalConfigId || config._parentOpts.verticalConfigId || null;
+  }
+
+  /**
+   * the component type
+   * @returns {string}
+   * @override
+   */
+  static get type () {
+    return 'AccordionResults';
+  }
+
+  /**
+   * The template to render
+   * @returns {string}
+   * @override
+   */
+  static defaultTemplateName (config) {
+    return 'results/resultsaccordion';
+  }
+
+  /**
+   * overrides onMount to add bindings to change the height on click
+   * @returns {AccordionResultsComponent}
+   * @override
+   */
+  onMount () {
+    super.onMount();
+
+    // NOTE(amullings): This is a hack, since currently components with siblings
+    // have no way of referring to their own element. We have to grab the first
+    // element since sections get added in reverse.
+    const selfEl = this._container.firstElementChild;
+
+    const accordionEls = DOM.queryAll(selfEl, this._selectorBase);
+    accordionEls.forEach((accordionEl) => {
+      const toggleEl = DOM.query(accordionEl, this.toggleSelector());
+      const contentEl = DOM.query(accordionEl, this.bodySelector());
+      this.changeHeight(contentEl, accordionEl);
+      toggleEl.addEventListener('click', () => {
+        this.handleClick(accordionEl, toggleEl, contentEl);
+      });
+    });
+
+    return this;
+  }
+
+  setState (data) {
+    return super.setState(Object.assign({}, data, {
+      modifier: this.verticalConfigId
+    }));
+  }
+
+  /**
+   * click handler for the accordion toggle button
+   * @param wrapperEl {HTMLElement} the toggle container
+   * @param toggleEl {HTMLElement} the button
+   * @param contentEl {HTMLElement} the toggle target
+   */
+  handleClick (wrapperEl, toggleEl, contentEl) {
+    const event = new AnalyticsEvent(this.isCollapsed(wrapperEl) ? 'ROW_EXPAND' : 'ROW_COLLAPSE')
+      .addOptions({
+        verticalConfigId: this.verticalConfigId,
+        entityId: toggleEl.dataset.entityId
+      });
+    wrapperEl.classList.toggle(this.collapsedClass);
+    this.changeHeight(contentEl, wrapperEl);
+    toggleEl.setAttribute('aria-expanded', this.isCollapsed(wrapperEl) ? 'false' : 'true');
+    this.analyticsReporter.report(event);
+  }
+
+  /**
+   * returns true if the element is currently collapsed
+   * @param wrapperEl {HTMLElement} the toggle container
+   * @returns {boolean}
+   */
+  isCollapsed (wrapperEl) {
+    if (!wrapperEl) {
+      return false;
+    }
+
+    return wrapperEl.classList.contains(this.collapsedClass);
+  }
+
+  /**
+   * toggles the height between 0 and the content height for smooth animation
+   * @param targetEl {HTMLElement}
+   * @param wrapperEl {HTMLElement}
+   */
+  changeHeight (targetEl, wrapperEl) {
+    targetEl.style.height = `${this.isCollapsed(wrapperEl) ? 0 : targetEl.scrollHeight}px`;
+  }
+
+  /**
+   * helper for composing child element selectors
+   * @param child {string}
+   * @returns {string}
+   */
+  buildSelector (child) {
+    return `${this._selectorBase}${child}`;
+  }
+
+  /**
+   * helper for the toggle button selector
+   * @returns {string}
+   */
+  toggleSelector () {
+    return this.buildSelector('-toggle');
+  }
+
+  /**
+   * helper for the content element selector
+   * @returns {string}
+   */
+  bodySelector () {
+    return this.buildSelector('-body');
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/ui_components_results_alternativeverticalscomponent.js.html b/docs/ui_components_results_alternativeverticalscomponent.js.html new file mode 100644 index 000000000..eb888b9c6 --- /dev/null +++ b/docs/ui_components_results_alternativeverticalscomponent.js.html @@ -0,0 +1,262 @@ + + + + + JSDoc: Source: ui/components/results/alternativeverticalscomponent.js + + + + + + + + + + +
+ +

Source: ui/components/results/alternativeverticalscomponent.js

+ + + + + + +
+
+
/** @module AlternativeVerticalsComponent */
+
+import AlternativeVertical from '../../../core/models/alternativevertical';
+import Component from '../component';
+import StorageKeys from '../../../core/storage/storagekeys';
+import { replaceUrlParams, filterParamsForExperienceLink } from '../../../core/utils/urlutils';
+import SearchParams from '../../dom/searchparams';
+
+export default class AlternativeVerticalsComponent extends Component {
+  constructor (opts = {}, systemOpts = {}) {
+    super(opts, systemOpts);
+
+    this.moduleId = StorageKeys.ALTERNATIVE_VERTICALS;
+
+    /**
+     * Alternative verticals that have results for the current query
+     * This gets updated based on the server results
+     * @type {AlternativeVerticals}
+     * @private
+     */
+    this._alternativeVerticals = (opts.data && opts.data.alternativeVerticals) || [];
+
+    /**
+     * Vertical pages config from global verticals config
+     * @type {VerticalPagesConfig}
+     * @private
+     */
+    this._verticalsConfig = opts.verticalsConfig || [];
+
+    /**
+     * The name of the vertical that is exposed for the link
+     * @type {string}
+     */
+    this._currentVerticalLabel = this.getCurrentVerticalLabel(opts.verticalsConfig) || '';
+
+    /**
+     * The alternative vertical search suggestions, parsed from alternative verticals and
+     * the global verticals config.
+     * This gets updated based on the server results
+     * @type {AlternativeVertical[]}
+     */
+    this.verticalSuggestions = this._buildVerticalSuggestions(
+      this._alternativeVerticals,
+      this._verticalsConfig,
+      this.core.storage.get(StorageKeys.API_CONTEXT),
+      this.core.storage.get(StorageKeys.REFERRER_PAGE_URL)
+    );
+
+    /**
+     * The url to the universal page to link back to without query params
+     * @type {string|null}
+     */
+    this._baseUniversalUrl = opts.baseUniversalUrl || '';
+
+    /**
+     * The url to the universal page to link back to with current query params
+     * @type {string|null}
+     */
+    this._universalUrl = this._getUniversalURL(
+      this._baseUniversalUrl,
+      new SearchParams(this.core.storage.getCurrentStateUrlMerged())
+    );
+
+    /**
+     * Whether or not results are displaying, used to control language in the info box
+     * @type {boolean}
+     */
+    this._isShowingResults = opts.isShowingResults || false;
+
+    const reRender = () => {
+      this.verticalSuggestions = this._buildVerticalSuggestions(
+        this._alternativeVerticals,
+        this._verticalsConfig,
+        this.core.storage.get(StorageKeys.API_CONTEXT),
+        this.core.storage.get(StorageKeys.REFERRER_PAGE_URL)
+      );
+      this._universalUrl = this._getUniversalURL(
+        this._baseUniversalUrl,
+        new SearchParams(this.core.storage.getCurrentStateUrlMerged())
+      );
+      this.setState(this.core.storage.get(StorageKeys.ALTERNATIVE_VERTICALS));
+    };
+
+    this.core.storage.registerListener({
+      eventType: 'update',
+      storageKey: StorageKeys.API_CONTEXT,
+      callback: reRender
+    });
+    this.core.storage.registerListener({
+      eventType: 'update',
+      storageKey: StorageKeys.SESSIONS_OPT_IN,
+      callback: reRender
+    });
+  }
+
+  static get type () {
+    return 'AlternativeVerticals';
+  }
+
+  /**
+   * The template to render
+   * @returns {string}
+   * @override
+   */
+  static defaultTemplateName (config) {
+    return 'results/alternativeverticals';
+  }
+
+  static areDuplicateNamesAllowed () {
+    return true;
+  }
+
+  setState (data) {
+    return super.setState(Object.assign({ verticalSuggestions: [] }, data, {
+      universalUrl: this._universalUrl,
+      verticalSuggestions: this.verticalSuggestions,
+      currentVerticalLabel: this._currentVerticalLabel,
+      isShowingResults: this._isShowingResults,
+      query: this.core.storage.get(StorageKeys.QUERY)
+    }));
+  }
+
+  getCurrentVerticalLabel (verticalsConfig) {
+    const thisVertical = verticalsConfig.find(config => {
+      return config.isActive || false;
+    });
+
+    return thisVertical ? thisVertical.label : '';
+  }
+
+  /**
+   * _buildVerticalSuggestions will construct an array of {AlternativeVertical}
+   * from alternative verticals and verticalPages configuration
+   * @param {object} alternativeVerticals alternativeVerticals server response
+   * @param {object} verticalsConfig the configuration to use
+   */
+  _buildVerticalSuggestions (alternativeVerticals, verticalsConfig, context, referrerPageUrl) {
+    let verticals = [];
+
+    const params = new SearchParams(this.core.storage.getCurrentStateUrlMerged());
+    if (context) {
+      params.set(StorageKeys.API_CONTEXT, context);
+    }
+    if (typeof referrerPageUrl === 'string') {
+      params.set(StorageKeys.REFERRER_PAGE_URL, referrerPageUrl);
+    }
+    const sessionsOptIn = this.core.storage.get(StorageKeys.SESSIONS_OPT_IN);
+    if (sessionsOptIn && sessionsOptIn.setDynamically) {
+      params[StorageKeys.SESSIONS_OPT_IN] = sessionsOptIn.value;
+    }
+
+    const filteredParams = filterParamsForExperienceLink(
+      params,
+      types => this.componentManager.getComponentNamesForComponentTypes(types)
+    );
+
+    for (const alternativeVertical of alternativeVerticals) {
+      const verticalKey = alternativeVertical.verticalConfigId;
+
+      const matchingVerticalConfig = verticalsConfig.find(config => {
+        return config.verticalKey === verticalKey;
+      });
+
+      if (!matchingVerticalConfig || alternativeVertical.resultsCount < 1) {
+        continue;
+      }
+
+      verticals.push(new AlternativeVertical({
+        label: matchingVerticalConfig.label,
+        url: replaceUrlParams(matchingVerticalConfig.url, filteredParams),
+        iconName: matchingVerticalConfig.icon,
+        iconUrl: matchingVerticalConfig.iconUrl,
+        resultsCount: alternativeVertical.resultsCount
+      }));
+    }
+
+    return verticals;
+  }
+
+  /**
+   * Adds parameters that are dynamically set. Removes parameters for facets,
+   * filters, and pagination, which should not persist across the experience.
+   * @param {string} baseUrl The url append the appropriate params to. Note:
+   *                         params already on the baseUrl will be stripped
+   * @param {SearchParams} params The parameters to include in the experience URL
+   * @return {string} The formatted experience URL with appropriate query params
+   */
+  _getUniversalURL (baseUrl, params) {
+    if (!baseUrl) {
+      return '';
+    }
+
+    params.set(StorageKeys.QUERY, this.core.storage.get(StorageKeys.QUERY));
+
+    const context = this.core.storage.get(StorageKeys.API_CONTEXT);
+    if (context) {
+      params.set(StorageKeys.API_CONTEXT, context);
+    }
+    const referrerPageUrl = this.core.storage.get(StorageKeys.REFERRER_PAGE_URL);
+    if (referrerPageUrl !== undefined) {
+      params.set(StorageKeys.REFERRER_PAGE_URL, referrerPageUrl);
+    }
+
+    const filteredParams = filterParamsForExperienceLink(
+      params,
+      types => this.componentManager.getComponentNamesForComponentTypes(types)
+    );
+
+    return replaceUrlParams(baseUrl, filteredParams);
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/ui_components_results_appliedfilterscomponent.js.html b/docs/ui_components_results_appliedfilterscomponent.js.html new file mode 100644 index 000000000..02d32fee0 --- /dev/null +++ b/docs/ui_components_results_appliedfilterscomponent.js.html @@ -0,0 +1,214 @@ + + + + + JSDoc: Source: ui/components/results/appliedfilterscomponent.js + + + + + + + + + + +
+ +

Source: ui/components/results/appliedfilterscomponent.js

+ + + + + + +
+
+
/** @module AppliedFiltersComponent */
+
+import Component from '../component';
+import StorageKeys from '../../../core/storage/storagekeys';
+import SearchStates from '../../../core/storage/searchstates';
+import DOM from '../../dom/dom';
+import { groupArray } from '../../../core/utils/arrayutils';
+import {
+  convertNlpFiltersToFilterNodes,
+  flattenFilterNodes,
+  pruneFilterNodes
+} from '../../../core/utils/filternodeutils';
+import QueryTriggers from '../../../core/models/querytriggers';
+
+const DEFAULT_CONFIG = {
+  showFieldNames: false,
+  showChangeFilters: false,
+  removable: false,
+  delimiter: '|',
+  labelText: 'Filters applied to this search:',
+  removableLabelText: 'Remove this filter',
+  hiddenFields: ['builtin.entityType']
+};
+
+export default class AppliedFiltersComponent extends Component {
+  constructor (config = {}, systemConfig = {}) {
+    super({ ...DEFAULT_CONFIG, ...config }, systemConfig);
+
+    this._verticalKey = this._config.verticalKey ||
+      this.core.storage.get(StorageKeys.SEARCH_CONFIG).verticalKey;
+
+    this.moduleId = StorageKeys.FACETS_LOADED;
+
+    this.core.storage.registerListener({
+      eventType: 'update',
+      storageKey: StorageKeys.VERTICAL_RESULTS,
+      callback: results => {
+        if (results.searchState === SearchStates.SEARCH_COMPLETE) {
+          this.setState();
+        }
+      }
+    });
+  }
+
+  static areDuplicateNamesAllowed () {
+    return true;
+  }
+
+  onMount () {
+    const removableFilterTags =
+      DOM.queryAll(this._container, '.js-yxt-AppliedFilters-removableFilterTag');
+    removableFilterTags.forEach(tag => {
+      DOM.on(tag, 'click', () => this._removeFilterTag(tag));
+    });
+  }
+
+  /**
+   * Call remove callback for the {@link FilterNode} corresponding to a specific
+   * removable filter tag.
+   * @param {HTMLElement} tag
+   */
+  _removeFilterTag (tag) {
+    const { filterId } = tag.dataset;
+    const filterNode = this.appliedFilterNodes[filterId];
+    filterNode.remove();
+    this.core.triggerSearch(QueryTriggers.FILTER_COMPONENT);
+  }
+
+  /**
+   * Returns the currently applied nlp filter nodes, with nlp filter nodes that
+   * are duplicates of other filter nodes removed or filter on hiddenFields removed.
+   * @returns {Array<FilterNode>}
+   */
+  _getPrunedNlpFilterNodes () {
+    const duplicatesRemoved = this.nlpFilterNodes.filter(nlpNode => {
+      const isDuplicate = this.appliedFilterNodes.find(appliedNode =>
+        appliedNode.hasSameFilterAs(nlpNode)
+      );
+      return !isDuplicate;
+    });
+    return pruneFilterNodes(duplicatesRemoved, this._config.hiddenFields);
+  }
+
+  /**
+   * Combine all of the applied filters into a format the handlebars
+   * template can work with.
+   * Keys are the fieldName of the filter. Values are an array of objects with a
+   * displayValue and dataFilterId.
+   * @returns {Array<Object>}
+   */
+  _groupAppliedFilters () {
+    const getFieldName = filterNode => filterNode.getMetadata().fieldName;
+    const parseNlpFilterDisplay = filterNode => ({
+      displayValue: filterNode.getMetadata().displayValue
+    });
+    const parseRemovableFilterDisplay = (filterNode, index) => ({
+      displayValue: filterNode.getMetadata().displayValue,
+      dataFilterId: index,
+      removable: this._config.removable
+    });
+    const removableNodes = groupArray(this.appliedFilterNodes, getFieldName, parseRemovableFilterDisplay);
+    const prunedNlpFilterNodes = this._getPrunedNlpFilterNodes();
+    return groupArray(prunedNlpFilterNodes, getFieldName, parseNlpFilterDisplay, removableNodes);
+  }
+
+  /**
+   * Returns an array of object the handlebars can understand and render
+   * the applied filters bar from. Our handlebars can only loop through arrays,
+   * not objects, so we need to reformat the grouped applied filters.
+   * @returns {Array<Object>}
+   */
+  _createAppliedFiltersArray () {
+    const groupedFilters = this._groupAppliedFilters();
+    return Object.keys(groupedFilters).map(label => ({
+      label: label,
+      filterDataArray: groupedFilters[label]
+    }));
+  }
+
+  /**
+   * Pulls applied filter nodes from {@link FilterRegistry}, then retrives an array of
+   * the leaf nodes, and then removes hidden or empty {@link FilterNode}s. Then appends
+   * the currently applied nlp filters.
+   */
+  _calculateAppliedFilterNodes () {
+    const filterNodes = this.core.filterRegistry.getAllFilterNodes();
+    const simpleFilterNodes = flattenFilterNodes(filterNodes);
+    return pruneFilterNodes(simpleFilterNodes, this._config.hiddenFields);
+  }
+
+  setState (data) {
+    const verticalResults = this.core.storage.get(StorageKeys.VERTICAL_RESULTS) || {};
+
+    /**
+     * Array of nlp filters in the search response.
+     * @type {Array<AppliedQueryFilter>}
+     */
+    const nlpFilters = verticalResults.appliedQueryFilters || [];
+
+    this.nlpFilterNodes = convertNlpFiltersToFilterNodes(nlpFilters);
+
+    this.appliedFilterNodes = this._calculateAppliedFilterNodes();
+    const appliedFiltersArray = this._createAppliedFiltersArray();
+
+    return super.setState({
+      ...data,
+      appliedFiltersArray: appliedFiltersArray
+    });
+  }
+
+  static get type () {
+    return 'AppliedFilters';
+  }
+
+  /**
+   * The template to render
+   * @returns {string}
+   * @override
+   */
+  static defaultTemplateName (config) {
+    return 'results/appliedfilters';
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/ui_components_results_directanswercomponent.js.html b/docs/ui_components_results_directanswercomponent.js.html index 55f40f1b9..fded0310d 100644 --- a/docs/ui_components_results_directanswercomponent.js.html +++ b/docs/ui_components_results_directanswercomponent.js.html @@ -29,16 +29,129 @@

Source: ui/components/results/directanswercomponent.js/** @module DirectAnswerComponent */ import Component from '../component'; +import AnalyticsEvent from '../../../core/analytics/analyticsevent'; import StorageKeys from '../../../core/storage/storagekeys'; +import DOM from '../../dom/dom'; +import TranslationFlagger from '../../i18n/translationflagger'; + +/** + * EventTypes are explicit strings defined + * for what the server expects for analytics. + * + * @enum + */ +const EventTypes = { + THUMBS_UP: 'THUMBS_UP', + THUMBS_DOWN: 'THUMBS_DOWN' +}; + +const DEFAULT_CONFIG = { + positiveFeedbackSrText: TranslationFlagger.flag({ + phrase: 'This answered my question' + }), + negativeFeedbackSrText: TranslationFlagger.flag({ + phrase: 'This did not answer my question' + }), + footerTextOnSubmission: TranslationFlagger.flag({ + phrase: 'Thank you for your feedback!' + }) +}; export default class DirectAnswerComponent extends Component { - constructor (opts = {}) { - super(opts); + constructor (config = {}, systemConfig = {}) { + super({ ...DEFAULT_CONFIG, ...config }, systemConfig); + + /** + * The user given config, without any defaults applied. + * @type {Object} + */ + this._userConfig = { ...config }; + /** + * Recieve updates from storage based on this index + * @type {StorageKey} + */ this.moduleId = StorageKeys.DIRECT_ANSWER; - this._templateName = 'results/directanswer'; + + /** + * The form used for submitting the feedback + * @type {string} + */ + this._formEl = config.formEl || '.js-directAnswer-feedback-form'; + + /** + * The `thumbs up` css selector to bind ui interaction to for reporting + * @type {string} + */ + this._thumbsUpSelector = config.thumbsUpSelector || '.js-directAnswer-thumbUp'; + + /** + * The `thumbs down` css selector to bind ui interaction to for reporting + * @type {string} + */ + this._thumbsDownSelector = config.thumbsDownSelector || '.js-directAnswer-thumbDown'; + + /** + * The display text for the View Details click to action link + * @type {string} + */ + this._viewDetailsText = config.viewDetailsText || TranslationFlagger.flag({ + phrase: 'View Details', + context: 'Button label, view is a verb' + }); + + /** + * The default custom direct answer card to use, when there are no matching card overrides. + * @type {string} + */ + this._defaultCard = config.defaultCard; + + /** + * Card overrides, which choose a custom direct answer card based on fieldName, fieldType, and entityType. + * @type {Array<Object>} + */ + this._cardOverrides = config.cardOverrides || []; + + /** + * Type options, which allows a card type to be specified based on the direct answer type. + * May contain cardOverrides. + * + * @example + * { + * 'FEATURED_SNIPPET': { + * cardType: 'documentsearch-standard', + * cardOverrides: [ + * { + * entityType: 'Person', + * cardType: 'custom-card' + * } + * ] + * } + * } + * + * @type {Object} + */ + this._types = config.types; + + this._validateTypes(); + } + + static get type () { + return 'DirectAnswer'; + } + + /** + * The template to render + * @returns {string} + * @override + */ + static defaultTemplateName (config) { + return 'results/directanswer'; } + /** + * beforeMount, only display the direct answer component if it has data + */ beforeMount () { if (!this.hasState('answer')) { return false; @@ -47,8 +160,237 @@

Source: ui/components/results/directanswercomponent.js { + const formEl = e.target; + const checkedValue = DOM.query(formEl, 'input:checked').value === 'true'; + + this.reportQuality(checkedValue); + this.updateState({ + 'feedbackSubmitted': true + }); + }); + + // Is this actually necessary? I guess it's only necessary if the + // submit button is hidden. + DOM.on(this._thumbsUpSelector, 'click', () => { DOM.trigger(this._formEl, 'submit'); }); + DOM.on(this._thumbsDownSelector, 'click', () => { DOM.trigger(this._formEl, 'submit'); }); + + const rtfElement = DOM.query(this._container, '.js-yxt-rtfValue'); + rtfElement && DOM.on(rtfElement, 'click', e => this._handleRtfClickAnalytics(e)); + } + + /** + * A click handler for links in a Rich Text Direct Answer. When such a link + * is clicked, an {@link AnalyticsEvent} needs to be fired. + * + * @param {MouseEvent} event The click event. + */ + _handleRtfClickAnalytics (event) { + if (!event.target.dataset.ctaType) { + return; + } + const ctaType = event.target.dataset.ctaType; + + const relatedItem = this.getState('relatedItem'); + const analyticsOptions = { + verticalKey: relatedItem.verticalConfigId, + directAnswer: true, + fieldName: this.getState('answer').fieldApiName, + searcher: 'UNIVERSAL', + entityId: relatedItem.data.id, + url: event.target.href + }; + + const analyticsEvent = new AnalyticsEvent(ctaType); + analyticsEvent.addOptions(analyticsOptions); + this.analyticsReporter.report(analyticsEvent); + } + + /** + * updateState enables for partial updates (the delta between the old and new) + * @type {object} The new state to apply to the old + */ + updateState (state = {}) { + const newState = Object.assign({}, this.getState(), state); + this.setState(newState); + } + + setState (data) { + return super.setState(Object.assign({}, data, { + eventOptions: this.eventOptions(data), + viewDetailsText: this._viewDetailsText, + directAnswer: data, + customCard: this._getCard(data) + })); + } + + eventOptions (data) { + if (!data || Object.keys(data).length === 0) { + return data; + } + return JSON.stringify({ + verticalConfigId: data.relatedItem.verticalConfigId, + searcher: 'UNIVERSAL', + entityId: data.relatedItem.data.id, + ctaLabel: this._viewDetailsText.toUpperCase().replace(' ', '_') + }); + } + + /** + * Determines the card that should be used for the given direct answer. + * + * @param {Object} directAnswer The direct answer state + * @returns {string} + */ + _getCard (directAnswer) { + if (this._types) { + return this._getCardBasedOnTypes(directAnswer); + } else if (this._cardOverrides.length > 0) { + return this._getCardBasedOnOverrides({ + directAnswer: directAnswer, + overrides: this._cardOverrides, + fallback: this._defaultCard + }); + } else { + return this._defaultCard; + } + } + + /** + * Determines the card that should be used based on the types option + * + * @param {Object} directAnswer The direct answer state + * @returns {string} + */ + _getCardBasedOnTypes (directAnswer) { + if (!('type' in directAnswer) || !(directAnswer.type in this._types)) { + return this._defaultCard; + } + + const typeOptions = this._types[directAnswer.type]; + + const cardFallback = typeOptions.cardType || this._defaultCard; + + if (typeOptions.cardOverrides) { + return this._getCardBasedOnOverrides({ + directAnswer: directAnswer, + overrides: typeOptions.cardOverrides, + fallback: cardFallback + }); + } + + return cardFallback; + } + + /** + * Returns the custom card type that should be used for the given direct answer. + * + * @param {Object} directAnswer The direct answer state + * @param {Object[]} overrides The overrides to search through + * @param {string} fallback The card to return if no match is found + * @returns {string} + */ + _getCardBasedOnOverrides ({ directAnswer, overrides, fallback }) { + const cardOverride = overrides.find(override => { + return this._overrideMatchesAnswer(override, directAnswer); + }); + return cardOverride ? cardOverride.cardType : fallback; + } + + /** + * Check whether a given cardOverride matches a given directAnswer. + * + * @param {Object} override + * @param {Object} directAnswer + * @returns {boolean} + */ + _overrideMatchesAnswer (override, directAnswer) { + if (!Object.keys(directAnswer).length) { + return true; + } + const directAnswerPropeties = { + type: directAnswer.type, + entityType: directAnswer.relatedItem.data.type, + fieldName: directAnswer.answer.fieldName, + fieldType: directAnswer.answer.fieldType + }; + for (let [propertyToMatch, propertyValue] of Object.entries(override)) { + if (propertyToMatch === 'cardType') { + continue; + } + if (directAnswerPropeties[propertyToMatch] !== propertyValue) { + return false; + } + } + return true; + } + + /** + * Throws an error if the types config option is not formatted properly. + * @throws if validation fails + */ + _validateTypes () { + if (!this._types) { + return; + } + + const validateSupportedKeysOfObject = (supportedKeys, object) => { + Object.keys(object).forEach(key => { + if (!supportedKeys.includes(key)) { + const supportedKeysString = supportedKeys.join(' and '); + throw new Error(`The key '${key}' is not a supported option. Supported options include ${supportedKeysString}.`); + } + }); + }; + + Object.entries(this._types).forEach(([directAnswerType, typeOptions]) => { + const supportedTypeOptions = ['cardType', 'cardOverrides']; + validateSupportedKeysOfObject(supportedTypeOptions, typeOptions); + if (!typeOptions.cardOverrides) { + return; + } + const supportedCardOverrideOptions = ['fieldName', 'entityType', 'fieldType', 'cardType']; + typeOptions.cardOverrides.forEach(overrideOptions => { + validateSupportedKeysOfObject(supportedCardOverrideOptions, overrideOptions); + }); + }); + } + + /** + * reportQuality will send the quality feedback to analytics + * @param {boolean} isGood true if the answer is what you were looking for + */ + reportQuality (isGood) { + const eventType = isGood === true ? EventTypes.THUMBS_UP : EventTypes.THUMBS_DOWN; + const event = new AnalyticsEvent(eventType) + .addOptions({ + 'directAnswer': true + }); + + this.analyticsReporter.report(event); + } + + addChild (data, type, opts) { + if (type === this.getState('customCard')) { + return super.addChild(this.getState('directAnswer'), type, { + ...this._userConfig, + ...opts + }); + } + return super.addChild(data, type, opts); } } @@ -61,13 +403,13 @@

Source: ui/components/results/directanswercomponent.js
- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/ui_components_results_eventresultsitemcomponent.js.html b/docs/ui_components_results_eventresultsitemcomponent.js.html deleted file mode 100644 index aaf5875c7..000000000 --- a/docs/ui_components_results_eventresultsitemcomponent.js.html +++ /dev/null @@ -1,70 +0,0 @@ - - - - - JSDoc: Source: ui/components/results/eventresultsitemcomponent.js - - - - - - - - - - -
- -

Source: ui/components/results/eventresultsitemcomponent.js

- - - - - - -
-
-
/** @module EventResultsItemComponent */
-
-import ResultsItemComponent from './resultsitemcomponent';
-
-export default class EventResultsItemComponent extends ResultsItemComponent {
-  constructor (opts = {}) {
-    super(opts);
-
-    this._templateName = 'results/eventresultsitem';
-  }
-
-  static get type () {
-    return 'EventResultsItemComponent';
-  }
-
-  static areDuplicateNamesAllowed () {
-    return true;
-  }
-}
-
-
-
- - - - -
- - - -
- -
- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) -
- - - - - diff --git a/docs/ui_components_results_locationresultsitemcomponent.js.html b/docs/ui_components_results_locationresultsitemcomponent.js.html deleted file mode 100644 index e684aca13..000000000 --- a/docs/ui_components_results_locationresultsitemcomponent.js.html +++ /dev/null @@ -1,70 +0,0 @@ - - - - - JSDoc: Source: ui/components/results/locationresultsitemcomponent.js - - - - - - - - - - -
- -

Source: ui/components/results/locationresultsitemcomponent.js

- - - - - - -
-
-
/** @module LocationResultsItemComponent */
-
-import ResultsItemComponent from './resultsitemcomponent';
-
-export default class LocationResultsItemComponent extends ResultsItemComponent {
-  constructor (opts = {}) {
-    super(opts);
-
-    this._templateName = 'results/locationresultsitem';
-  }
-
-  static get type () {
-    return 'LocationResultsItemComponent';
-  }
-
-  static areDuplicateNamesAllowed () {
-    return true;
-  }
-}
-
-
-
- - - - -
- - - -
- -
- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) -
- - - - - diff --git a/docs/ui_components_results_paginationcomponent.js.html b/docs/ui_components_results_paginationcomponent.js.html new file mode 100644 index 000000000..7f9002449 --- /dev/null +++ b/docs/ui_components_results_paginationcomponent.js.html @@ -0,0 +1,378 @@ + + + + + JSDoc: Source: ui/components/results/paginationcomponent.js + + + + + + + + + + +
+ +

Source: ui/components/results/paginationcomponent.js

+ + + + + + +
+
+
/** @module PaginationComponent */
+
+import Component from '../component';
+import StorageKeys from '../../../core/storage/storagekeys';
+import DOM from '../../dom/dom';
+import { AnswersComponentError } from '../../../core/errors/errors';
+import SearchStates from '../../../core/storage/searchstates';
+import ResultsContext from '../../../core/storage/resultscontext';
+import TranslationFlagger from '../../i18n/translationflagger';
+
+export default class PaginationComponent extends Component {
+  constructor (config = {}, systemConfig = {}) {
+    super(config, systemConfig);
+
+    /**
+     * The vertical key to use for searches
+     * @type {string}
+     * @private
+     */
+    this._verticalKey = config.verticalKey || this.core.storage.get(StorageKeys.SEARCH_CONFIG).verticalKey;
+    if (typeof this._verticalKey !== 'string') {
+      throw new AnswersComponentError(
+        'verticalKey not provided, but necessary for pagination',
+        'PaginationComponent');
+    }
+
+    /**
+     * The number of pages visible before/after the current page on desktop.
+     * @type {number}
+     * @private
+     */
+    this._maxVisiblePagesDesktop = config.maxVisiblePagesDesktop === undefined ? 1 : config.maxVisiblePagesDesktop;
+
+    /**
+     * The number of pages visible before/after the current page on mobile.
+     * @type {number}
+     * @private
+     */
+    this._maxVisiblePagesMobile = config.maxVisiblePagesMobile === undefined ? 1 : config.maxVisiblePagesMobile;
+
+    /**
+     * If true, displays the first and last page buttons
+     * @type {boolean}
+     * @private
+     */
+    this._showFirstAndLastPageButtons = config.showFirstAndLastButton === undefined ? true : config.showFirstAndLastButton;
+
+    /**
+     * DEPRECATED
+     * @type {boolean}
+     * @private
+     */
+    this._firstPageButtonEnabled = config.showFirst === undefined ? this._showFirstAndLastPageButtons : config.showFirst;
+
+    /**
+     * DEPRECATED
+     * @type {boolean}
+     * @private
+     */
+    this._lastPageButtonEnabled = config.showLast === undefined ? this._showFirstAndLastPageButtons : config.showLast;
+
+    /**
+     * If true, always displays the page numbers for first and last page.
+     * @type {boolean}
+     * @private
+     */
+    this._pinFirstAndLastPage = config.pinFirstAndLastPage === undefined ? false : config.pinFirstAndLastPage;
+
+    /**
+     * Icons object for first, previous, next, and last page icons.
+     * @type {{
+     *  nextButtonIcon: (string | undefined),
+     *  previousButtonIcon: (string | undefined),
+     *  firstButtonIcon: (string | undefined),
+     *  lastButtonIcon: (string | undefined),
+     * }}
+     * @private
+     */
+    this._icons = config.icons;
+
+    /**
+     * Options to include with all analytic events sent by this component
+     * @type {object}
+     * @private
+     */
+    this._analyticsOptions = {
+      verticalKey: this._verticalKey
+    };
+
+    /**
+     * Label for a page of results.
+     * @type {string}
+     * @private
+     */
+    this._pageLabel = config.pageLabel !== undefined
+      ? config.pageLabel
+      : TranslationFlagger.flag({
+        phrase: 'Page',
+        context: 'Noun, a page of results'
+      });
+
+    /**
+     * Function that is invoked on pagination
+     * @type {function(): {}}
+     * @private
+     */
+    this._onPaginate = config.onPaginate || this.scrollToTop;
+
+    /**
+     * The maximum number of results per page
+     * @type {number}
+     * @private
+     */
+    this._limit = this.core.storage.get(StorageKeys.SEARCH_CONFIG).limit;
+
+    const offset = this.core.storage.get(StorageKeys.SEARCH_OFFSET) || 0;
+    this.core.storage.set(StorageKeys.SEARCH_OFFSET, Number(offset));
+    this.core.storage.registerListener({
+      eventType: 'update',
+      storageKey: StorageKeys.SEARCH_OFFSET,
+      callback: offset => {
+        if (typeof offset === 'number') {
+          return;
+        }
+        this.core.storage.set(StorageKeys.SEARCH_OFFSET, Number(offset));
+      }
+    });
+
+    this.core.storage.registerListener({
+      eventType: 'update',
+      storageKey: StorageKeys.VERTICAL_RESULTS,
+      callback: results => {
+        if (results.searchState === SearchStates.SEARCH_COMPLETE) {
+          this.setState();
+        }
+      }
+    });
+
+    /**
+     * Configuration for the behavior when there are no vertical results.
+     */
+    this._noResults = config.noResults ||
+      this.core.storage.get(StorageKeys.NO_RESULTS_CONFIG) ||
+      {};
+  }
+
+  static get type () {
+    return 'Pagination';
+  }
+
+  static defaultTemplateName () {
+    return 'results/pagination';
+  }
+
+  shouldShowControls (results, limit) {
+    const hasResults = results.searchState === 'search-complete' && results.resultsCount > limit;
+    const isNormalResults = results.resultsContext === ResultsContext.NORMAL;
+    const isVisibleForNoResults = 'visible' in this._noResults
+      ? this._noResults.visible
+      : this._noResults.displayAllResults;
+    return hasResults && (isNormalResults || isVisibleForNoResults);
+  }
+
+  onMount () {
+    const results = this.core.storage.get(StorageKeys.VERTICAL_RESULTS) || {};
+    const limit = this.core.storage.get(StorageKeys.SEARCH_CONFIG).limit;
+    const showControls = this.shouldShowControls(results, limit);
+    const offset = this.core.storage.get(StorageKeys.SEARCH_OFFSET) || 0;
+    if (!showControls) {
+      return;
+    }
+
+    const previousPageButton = DOM.query(this._container, '.js-yxt-Pagination-previous');
+    const nextPageButton = DOM.query(this._container, '.js-yxt-Pagination-next');
+    const maxPage = Math.trunc((results.resultsCount - 1) / limit);
+
+    DOM.on(previousPageButton, 'click', () => this.updatePage(offset - limit));
+    DOM.on(nextPageButton, 'click', () => this.updatePage(offset + limit));
+
+    if (this._firstPageButtonEnabled) {
+      const firstPageButton = DOM.query(this._container, '.js-yxt-Pagination-first');
+      DOM.on(firstPageButton, 'click', () => this.updatePage(0));
+    }
+
+    if (this._lastPageButtonEnabled) {
+      const lastPageButton = DOM.query(this._container, '.js-yxt-Pagination-last');
+      DOM.on(lastPageButton, 'click', () => this.updatePage(maxPage * limit));
+    }
+
+    DOM.queryAll('.js-yxt-Pagination-link').forEach(node => {
+      DOM.on(node, 'click', () => this.updatePage((parseInt(node.dataset.number) - 1) * limit));
+    });
+  }
+
+  updatePage (offset) {
+    const results = this.core.storage.get(StorageKeys.VERTICAL_RESULTS) || {};
+    const currentOffset = this.core.storage.get(StorageKeys.SEARCH_OFFSET) || 0;
+    const currentPageNumber = (currentOffset / this._limit) + 1;
+    const newPageNumber = (offset / this._limit) + 1;
+    const maxPageCount = this._computeMaxPage(results.resultsCount);
+    this._onPaginate(newPageNumber, currentPageNumber, maxPageCount);
+    this.core.storage.setWithPersist(StorageKeys.SEARCH_OFFSET, offset);
+    this.core.verticalPage();
+  }
+
+  scrollToTop () {
+    document.documentElement.scrollTop = 0;
+    // Safari
+    document.body.scrollTop = 0;
+  }
+
+  /**
+   * Computes the highest page number for a given amount of results
+   * @param {number} resultsCount
+   */
+  _computeMaxPage (resultsCount) {
+    return Math.trunc((resultsCount - 1) / this._limit) + 1;
+  }
+
+  /**
+   * Pagination should evenly add page numbers in the "forward" and "backward" directions, unless
+   * one side has reached the max/min value, in which case the remaining side should be the only
+   * one to get more pages.
+   * @param {number} pageNumber the current page's number
+   * @param {number} maxPage the highest page number, acts as the upper bound
+   * @param {number} limit the maximum total number of pages that are allocated
+   * @returns {Array<number>} the backLimit and frontLimit, respectively
+   */
+  _allocate (pageNumber, maxPage, limit) {
+    var backLimit = pageNumber;
+    var frontLimit = pageNumber;
+    for (var i = 0; i < limit; i++) {
+      if (i % 2 === 0) {
+        if (backLimit > 0) {
+          backLimit--;
+        } else if (frontLimit < maxPage) {
+          frontLimit++;
+        }
+      } else {
+        if (frontLimit < maxPage) {
+          frontLimit++;
+        } else if (backLimit > 0) {
+          backLimit--;
+        }
+      }
+    }
+
+    return [backLimit, frontLimit];
+  }
+
+  /**
+   * Creates an object representing the view state of the page numbers and ellipses
+   * @param {number} pageNumber refers to the page number, not the page index
+   * @param {number} maxPage the highest page number, which also represents the total page count
+   * @returns {Object} the view-model for the page numbers displayed in the component, including whether to display ellipses
+   */
+  _createPageNumberViews (pageNumber, maxPage) {
+    const [mobileBackLimit, mobileFrontLimit] = this._allocate(pageNumber, maxPage, this._maxVisiblePagesMobile);
+    const [desktopBackLimit, desktopFrontLimit] = this._allocate(pageNumber, maxPage, this._maxVisiblePagesDesktop);
+    const pageNumberViews = [];
+    for (var i = 1; i <= maxPage; i++) {
+      const num = { number: i };
+      if (i === pageNumber) {
+        num.active = true;
+        if (this._maxVisiblePagesDesktop > 1) {
+          num.activeDesktop = true;
+        }
+        if (this._maxVisiblePagesMobile > 1) {
+          num.activeMobile = true;
+        }
+      } else {
+        if (i <= mobileBackLimit || i > mobileFrontLimit) {
+          num.mobileHidden = true;
+        }
+        if (i <= desktopBackLimit || i > desktopFrontLimit) {
+          num.desktopHidden = true;
+        }
+      }
+      pageNumberViews.push(num);
+    }
+
+    return {
+      pinnedNumbers: {
+        mobileBack: this._pinFirstAndLastPage && mobileBackLimit > 0,
+        mobileFront: this._pinFirstAndLastPage && mobileFrontLimit < maxPage,
+        desktopBack: this._pinFirstAndLastPage && desktopBackLimit > 0,
+        desktopFront: this._pinFirstAndLastPage && desktopFrontLimit < maxPage
+      },
+      ellipses: {
+        mobileBack: this._pinFirstAndLastPage && mobileBackLimit > 1,
+        mobileFront: this._pinFirstAndLastPage && mobileFrontLimit < maxPage - 1,
+        desktopBack: this._pinFirstAndLastPage && desktopBackLimit > 1,
+        desktopFront: this._pinFirstAndLastPage && desktopFrontLimit < maxPage - 1
+      },
+      pageNumberViews
+    };
+  }
+
+  setState (data) {
+    const results = this.core.storage.get(StorageKeys.VERTICAL_RESULTS) || {};
+    const offset = this.core.storage.get(StorageKeys.SEARCH_OFFSET) || 0;
+    const pageNumber = (offset / this._limit) + 1;
+    const isMoreResults = results.resultsCount > offset + this._limit;
+    const maxPage = this._computeMaxPage(results.resultsCount);
+    const { pinnedNumbers, ellipses, pageNumberViews } = this._createPageNumberViews(pageNumber, maxPage);
+
+    return super.setState({
+      showControls: this.shouldShowControls(results, this._limit),
+      firstPageButtonEnabled: this._firstPageButtonEnabled,
+      lastPageButtonEnabled: this._lastPageButtonEnabled,
+      pageNumber,
+      pageLabel: this._pageLabel,
+      showFirstPageButton: pageNumber > 2,
+      showPreviousPageButton: pageNumber > 1,
+      showNextPageButton: isMoreResults,
+      showLastPageButton: pageNumber < maxPage - 1,
+      icons: this._icons,
+      pageNumbers: pageNumberViews,
+      pinnedNumbers,
+      ellipses,
+      pinPages: this._pinFirstAndLastPage,
+      nextPage: pageNumber + 1,
+      maxPage,
+      ...data
+    });
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/ui_components_results_peopleresultsitemcomponent.js.html b/docs/ui_components_results_peopleresultsitemcomponent.js.html deleted file mode 100644 index a04029177..000000000 --- a/docs/ui_components_results_peopleresultsitemcomponent.js.html +++ /dev/null @@ -1,70 +0,0 @@ - - - - - JSDoc: Source: ui/components/results/peopleresultsitemcomponent.js - - - - - - - - - - -
- -

Source: ui/components/results/peopleresultsitemcomponent.js

- - - - - - -
-
-
/** @module PeopleResultsItemComponent */
-
-import ResultsItemComponent from './resultsitemcomponent';
-
-export default class PeopleResultsItemComponent extends ResultsItemComponent {
-  constructor (opts = {}) {
-    super(opts);
-
-    this._templateName = 'results/peopleresultsitem';
-  }
-
-  static get type () {
-    return 'PeopleResultsItemComponent';
-  }
-
-  static areDuplicateNamesAllowed () {
-    return true;
-  }
-}
-
-
-
- - - - -
- - - -
- -
- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) -
- - - - - diff --git a/docs/ui_components_results_resultscomponent.js.html b/docs/ui_components_results_resultscomponent.js.html deleted file mode 100644 index b935b5a50..000000000 --- a/docs/ui_components_results_resultscomponent.js.html +++ /dev/null @@ -1,227 +0,0 @@ - - - - - JSDoc: Source: ui/components/results/resultscomponent.js - - - - - - - - - - -
- -

Source: ui/components/results/resultscomponent.js

- - - - - - -
-
-
/** @module ResultsComponent */
-
-import Component from '../component';
-
-import ResultsItemComponent from './resultsitemcomponent';
-import LocationResultsItemComponent from './locationresultsitemcomponent';
-import EventResultsItemComponent from './eventresultsitemcomponent';
-import PeopleResultsItemComponent from './peopleresultsitemcomponent';
-import MapComponent from '../map/mapcomponent';
-import StorageKeys from '../../../core/storage/storagekeys';
-
-const ResultType = {
-  EVENT: 'event',
-  LOCATION: 'location',
-  PEOPLE: 'people'
-};
-
-export default class ResultsComponent extends Component {
-  constructor (opts = {}) {
-    super(opts);
-
-    this.moduleId = StorageKeys.VERTICAL_RESULTS;
-    this._templateName = 'results/results';
-    this.limit = opts.limit || 5;
-    this._itemConfig = {
-      global: {
-        render: null,
-        template: null
-      },
-      [EventResultsItemComponent.type]: {
-        render: null,
-        template: null
-      },
-      [LocationResultsItemComponent.type]: {
-        render: null,
-        template: null
-      },
-      [PeopleResultsItemComponent.type]: {
-        render: null,
-        template: null
-      }
-    };
-
-    if (opts.renderItem === undefined && opts._parentOpts !== undefined) {
-      opts.renderItem = opts._parentOpts.renderItem;
-    }
-
-    if (opts.itemTemplate === undefined && opts._parentOpts !== undefined) {
-      opts.itemTemplate = opts._parentOpts.itemTemplate;
-    }
-
-    this.configureItem({
-      render: opts.renderItem,
-      template: opts.itemTemplate
-    });
-  }
-
-  mount () {
-    if (Object.keys(this.getState()).length > 0) {
-      super.mount();
-    }
-
-    return this;
-  }
-
-  static get duplicatesAllowed () {
-    return true;
-  }
-
-  setState (data, val) {
-    if (Object.keys(data).length === 0) {
-      return this;
-    }
-
-    return super.setState(Object.assign({}, data, {
-      includeMap: this._opts.includeMap,
-      mapConfig: this._opts.mapConfig
-    }), val);
-  }
-
-  static get type () {
-    return 'VerticalResults';
-  }
-
-  configureItem (config) {
-    if (typeof config.render === 'function') {
-      this._itemConfig.global.render = config.render;
-    } else {
-      for (let key in config.render) {
-        this.setItemRender(key, config.render[key]);
-      }
-    }
-
-    if (typeof config.template === 'string') {
-      this._itemConfig.global.template = config.template;
-    } else {
-      for (let key in config.template) {
-        this.setItemTemplate(key, config.template[key]);
-      }
-    }
-  }
-
-  setItemTemplate (type, template) {
-    let clazz = this.getItemComponent(type);
-    this._itemConfig[clazz.type].template = template;
-  }
-
-  setItemRender (type, render) {
-    let clazz = this.getItemComponent(type);
-    this._itemConfig[clazz.type].render = render;
-  }
-
-  getItemComponent (type) {
-    let comp = ResultsItemComponent;
-    switch (type) {
-      case ResultType.EVENT:
-        comp = EventResultsItemComponent;
-        break;
-      case ResultType.LOCATION:
-        comp = LocationResultsItemComponent;
-        break;
-      case ResultType.PEOPLE:
-        comp = PeopleResultsItemComponent;
-        break;
-    }
-
-    return comp;
-  }
-
-  addChild (data, type, opts) {
-    // TODO(billy) Refactor the way configuration and data flows
-    // through top level components to child components.
-    if (type === ResultsItemComponent.type) {
-      let clazz = this.getItemComponent(data.type);
-      if (clazz) {
-        type = clazz.type;
-      }
-    } else if (type === MapComponent.type) {
-      data = {
-        map: data
-      };
-      const newOpts = Object.assign({}, this._opts.mapConfig, opts);
-      return super.addChild(data, type, newOpts);
-    }
-
-    // Apply the proper item renders to the the components
-    // have just been constructed. Prioritize global over individual items.
-    let comp = super.addChild(data, type, opts);
-    let globalConfig = this._itemConfig.global;
-    let itemConfig = this._itemConfig[comp.type];
-    let hasGlobalRender = typeof globalConfig.render === 'function';
-    let hasGlobalTemplate = typeof globalConfig.template === 'string';
-
-    if (hasGlobalRender) {
-      comp.setRender(globalConfig.render);
-    }
-
-    if (hasGlobalTemplate) {
-      comp.setTemplate(globalConfig.template);
-    }
-
-    if (!itemConfig) {
-      return comp;
-    }
-
-    if (!hasGlobalRender && itemConfig.render) {
-      comp.setRender(itemConfig.render);
-    }
-
-    // Apply template specific situation
-    if (!hasGlobalTemplate && itemConfig.template) {
-      comp.setTemplate(itemConfig.template);
-    }
-    return comp;
-  }
-}
-
-
-
- - - - -
- - - -
- -
- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) -
- - - - - diff --git a/docs/ui_components_results_resultsheadercomponent.js.html b/docs/ui_components_results_resultsheadercomponent.js.html new file mode 100644 index 000000000..afec230a4 --- /dev/null +++ b/docs/ui_components_results_resultsheadercomponent.js.html @@ -0,0 +1,240 @@ + + + + + JSDoc: Source: ui/components/results/resultsheadercomponent.js + + + + + + + + + + +
+ +

Source: ui/components/results/resultsheadercomponent.js

+ + + + + + +
+
+
/** @module AppliedFiltersComponent */
+
+import Component from '../component';
+import StorageKeys from '../../../core/storage/storagekeys';
+import DOM from '../../dom/dom';
+import { groupArray } from '../../../core/utils/arrayutils';
+import {
+  convertNlpFiltersToFilterNodes,
+  flattenFilterNodes,
+  pruneFilterNodes
+} from '../../../core/utils/filternodeutils';
+import TranslationFlagger from '../../i18n/translationflagger';
+import QueryTriggers from '../../../core/models/querytriggers';
+
+const DEFAULT_CONFIG = {
+  showResultCount: true,
+  showAppliedFilters: true,
+  showFieldNames: false,
+  resultsCountSeparator: '|',
+  verticalURL: undefined,
+  showChangeFilters: false,
+  removable: false,
+  delimiter: '|',
+  isUniversal: false,
+  labelText: TranslationFlagger.flag({
+    phrase: 'Filters applied to this search:'
+  }),
+  removableLabelText: TranslationFlagger.flag({
+    phrase: 'Remove this filter',
+    context: 'Button label'
+  }),
+  resultsCountTemplate: '',
+  hiddenFields: []
+};
+
+export default class ResultsHeaderComponent extends Component {
+  constructor (config = {}, systemConfig = {}) {
+    super({ ...DEFAULT_CONFIG, ...config }, systemConfig);
+
+    const data = config.data || {};
+
+    /**
+     * Total number of results.
+     * @type {number}
+     */
+    this.resultsCount = data.resultsCount || 0;
+
+    /**
+     * Number of results displayed on the page.
+     * @type {number}
+     */
+    this.resultsLength = data.resultsLength || 0;
+
+    /**
+     * The compiled custom results count template, if the user specifies one.
+     * @type {Function}
+     */
+    this._compiledResultsCountTemplate = this._renderer.compile(this._config.resultsCountTemplate);
+
+    /**
+     * Array of nlp filters in the search response.
+     * @type {Array<AppliedQueryFilter>}
+     */
+    this.nlpFilterNodes = convertNlpFiltersToFilterNodes(data.nlpFilters || []);
+
+    this.moduleId = StorageKeys.DYNAMIC_FILTERS;
+  }
+
+  static areDuplicateNamesAllowed () {
+    return true;
+  }
+
+  onMount () {
+    const removableFilterTags =
+      DOM.queryAll(this._container, '.js-yxt-ResultsHeader-removableFilterTag');
+    removableFilterTags.forEach(tag => {
+      DOM.on(tag, 'click', () => this._removeFilterTag(tag));
+    });
+  }
+
+  /**
+   * Call remove callback for the {@link FilterNode} corresponding to a specific
+   * removable filter tag.
+   * @param {HTMLElement} tag
+   */
+  _removeFilterTag (tag) {
+    const { filterId } = tag.dataset;
+    const filterNode = this.appliedFilterNodes[filterId];
+    filterNode.remove();
+    this.core.triggerSearch(QueryTriggers.FILTER_COMPONENT);
+  }
+
+  /**
+   * Returns the currently applied nlp filter nodes, with nlp filter nodes that
+   * are duplicates of other filter nodes removed or filter on hiddenFields removed.
+   * @returns {Array<FilterNode>}
+   */
+  _getPrunedNlpFilterNodes () {
+    const duplicatesRemoved = this.nlpFilterNodes.filter(nlpNode => {
+      const isDuplicate = this.appliedFilterNodes.find(appliedNode =>
+        appliedNode.hasSameFilterAs(nlpNode)
+      );
+      return !isDuplicate;
+    });
+    return pruneFilterNodes(duplicatesRemoved, this._config.hiddenFields);
+  }
+
+  /**
+   * Combine all of the applied filters into a format the handlebars
+   * template can work with.
+   * Keys are the fieldName of the filter. Values are an array of objects with a
+   * displayValue and dataFilterId.
+   * TODO (SPR-2350): give every node a unique id, and use that instead of index for
+   * dataFilterId.
+   * @returns {Array<Object>}
+   */
+  _groupAppliedFilters () {
+    const getFieldName = filterNode => filterNode.getMetadata().fieldName;
+    const parseNlpFilterDisplay = filterNode => ({
+      displayValue: filterNode.getMetadata().displayValue
+    });
+    const parseRemovableFilterDisplay = (filterNode, index) => ({
+      displayValue: filterNode.getMetadata().displayValue,
+      dataFilterId: index,
+      removable: this._config.removable
+    });
+    const removableNodes = groupArray(this.appliedFilterNodes, getFieldName, parseRemovableFilterDisplay);
+    const prunedNlpFilterNodes = this._getPrunedNlpFilterNodes();
+    return groupArray(prunedNlpFilterNodes, getFieldName, parseNlpFilterDisplay, removableNodes);
+  }
+
+  /**
+   * Returns an array of object the handlebars can understand and render
+   * the applied filters bar from. Our handlebars can only loop through arrays,
+   * not objects, so we need to reformat the grouped applied filters.
+   * @returns {Array<Object>}
+   */
+  _createAppliedFiltersArray () {
+    const groupedFilters = this._groupAppliedFilters();
+    return Object.keys(groupedFilters).map(label => ({
+      label: label,
+      filterDataArray: groupedFilters[label]
+    }));
+  }
+
+  /**
+   * Pulls applied filter nodes from {@link FilterRegistry}, then retrives an array of
+   * the leaf nodes, and then removes hidden or empty {@link FilterNode}s. Then appends
+   * the currently applied nlp filters.
+   */
+  _calculateAppliedFilterNodes () {
+    const filterNodes = this.core.filterRegistry.getAllFilterNodes();
+    const simpleFilterNodes = flattenFilterNodes(filterNodes);
+    return pruneFilterNodes(simpleFilterNodes, this._config.hiddenFields);
+  }
+
+  setState (data) {
+    const offset = this.core.storage.get(StorageKeys.SEARCH_OFFSET) || 0;
+    this.appliedFilterNodes = this._calculateAppliedFilterNodes();
+    const appliedFiltersArray = this._createAppliedFiltersArray();
+    const shouldShowFilters = appliedFiltersArray.length > 0 && this._config.showAppliedFilters;
+    const resultsCountData = {
+      resultsCount: this.resultsCount,
+      resultsCountStart: offset + 1,
+      resultsCountEnd: offset + this.resultsLength
+    };
+    return super.setState({
+      ...data,
+      ...resultsCountData,
+      showResultSeparator: this._config.resultsCountSeparator && this._config.showResultCount && shouldShowFilters,
+      shouldShowFilters: shouldShowFilters,
+      appliedFiltersArray: appliedFiltersArray,
+      customResultsCount: this._compiledResultsCountTemplate(resultsCountData)
+    });
+  }
+
+  static get type () {
+    return 'ResultsHeader';
+  }
+
+  /**
+   * The template to render
+   * @returns {string}
+   * @override
+   */
+  static defaultTemplateName (config) {
+    return 'results/resultsheader';
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/ui_components_results_resultsitemcomponent.js.html b/docs/ui_components_results_resultsitemcomponent.js.html deleted file mode 100644 index 6dfdcd7ab..000000000 --- a/docs/ui_components_results_resultsitemcomponent.js.html +++ /dev/null @@ -1,70 +0,0 @@ - - - - - JSDoc: Source: ui/components/results/resultsitemcomponent.js - - - - - - - - - - -
- -

Source: ui/components/results/resultsitemcomponent.js

- - - - - - -
-
-
/** @module ResultsItemComponent */
-
-import Component from '../component';
-
-export default class ResultsItemComponent extends Component {
-  constructor (opts = {}) {
-    super(opts);
-
-    this._templateName = 'results/resultsitem';
-  }
-
-  static get type () {
-    return 'ResultsItemComponent';
-  }
-
-  static areDuplicateNamesAllowed () {
-    return true;
-  }
-}
-
-
-
- - - - -
- - - -
- -
- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) -
- - - - - diff --git a/docs/ui_components_results_universalresultscomponent.js.html b/docs/ui_components_results_universalresultscomponent.js.html index 048b114b6..b31348d7f 100644 --- a/docs/ui_components_results_universalresultscomponent.js.html +++ b/docs/ui_components_results_universalresultscomponent.js.html @@ -29,41 +29,154 @@

Source: ui/components/results/universalresultscomponent.j
/** @module UniversalResultsComponent */
 
 import Component from '../component';
+
 import StorageKeys from '../../../core/storage/storagekeys';
+import SearchStates from '../../../core/storage/searchstates';
+import AccordionResultsComponent from './accordionresultscomponent.js';
+import { defaultConfigOption } from '../../../core/utils/configutils';
+import TranslationFlagger from '../../i18n/translationflagger';
+import { getContainerClass } from '../../../core/utils/resultsutils';
 
 export default class UniversalResultsComponent extends Component {
-  constructor (opts = {}) {
-    super(opts);
-
+  constructor (config = {}, systemConfig = {}) {
+    super(config, systemConfig);
     this.moduleId = StorageKeys.UNIVERSAL_RESULTS;
-    this._templateName = 'results/universalresults';
-    this._limit = opts.limit || 10;
+    this._appliedFilters = {
+      show: true,
+      showFieldNames: false,
+      hiddenFields: ['builtin.entityType'],
+      resultsCountSeparator: '|',
+      showChangeFilters: false,
+      delimiter: '|',
+      labelText: TranslationFlagger.flag({
+        phrase: 'Filters applied to this search:'
+      }),
+      ...config.appliedFilters
+    };
+
+    const reRender = () =>
+      this.setState(this.core.storage.get(StorageKeys.UNIVERSAL_RESULTS) || {});
+    this.core.storage.registerListener({
+      eventType: 'update',
+      storageKey: StorageKeys.API_CONTEXT,
+      callback: reRender
+    });
+    this.core.storage.registerListener({
+      eventType: 'update',
+      storageKey: StorageKeys.SESSIONS_OPT_IN,
+      callback: reRender
+    });
   }
 
   static get type () {
     return 'UniversalResults';
   }
 
+  static defaultTemplateName (config) {
+    return 'results/universalresults';
+  }
+
   static areDuplicateNamesAllowed () {
     return true;
   }
 
-  init (opts) {
-    super.init(opts);
-    return this;
+  /**
+   * Updates the search state css class on this component's container.
+   */
+  updateContainerClass (searchState) {
+    Object.values(SearchStates).forEach(searchState => {
+      this.removeContainerClass(getContainerClass(searchState));
+    });
+
+    this.addContainerClass(getContainerClass(searchState));
+  }
+
+  setState (data, val) {
+    const sections = data.sections || [];
+    const query = this.core.storage.get(StorageKeys.QUERY);
+    const searchState = data.searchState || SearchStates.PRE_SEARCH;
+    this.updateContainerClass(searchState);
+    return super.setState(Object.assign(data, {
+      isPreSearch: searchState === SearchStates.PRE_SEARCH,
+      isSearchLoading: searchState === SearchStates.SEARCH_LOADING,
+      isSearchComplete: searchState === SearchStates.SEARCH_COMPLETE,
+      showNoResults: sections.length === 0 && (query || query === ''),
+      query: query,
+      sections: sections
+    }, val));
   }
 
-  addChild (data = {}, type) {
-    let opts = this.getChildConfig([data['verticalConfigId']]);
-    return super.addChild(data, type, opts);
+  addChild (data = {}, type, opts) {
+    const verticals = this._config.verticals || this._config.config || {};
+    const verticalKey = data.verticalConfigId;
+    const childOpts = {
+      ...opts,
+      ...UniversalResultsComponent.getChildConfig(
+        verticalKey, verticals[verticalKey] || {}, this._appliedFilters)
+    };
+    const childType = childOpts.useAccordion ? AccordionResultsComponent.type : type;
+    return super.addChild(data, childType, childOpts);
   }
 
-  getChildConfig (configId) {
-    let config = this._opts.config;
-    if (config === undefined) {
-      return {};
-    }
-    return this._opts['config'][configId] || this._opts['config'];
+  /**
+   * Applies synonyms and default config for a vertical in universal results.
+   * @param {string} verticalKey
+   * @param {Object} config
+   * @param {Object} topLevelAppliedFilters
+   * @returns {Object}
+   */
+  static getChildConfig (verticalKey, config, topLevelAppliedFilters) {
+    return {
+      // Tells vertical results it is in a universal results page.
+      isUniversal: true,
+      // Label for the vertical in the titlebar.
+      title: config.sectionTitle || verticalKey,
+      // Icon in the titlebar
+      icon: config.sectionTitleIconName || config.sectionTitleIconUrl,
+      // Url that links to the vertical search for this vertical.
+      verticalURL: config.url,
+      // Show a view more link by default, which also links to verticalURL.
+      viewMore: true,
+      // By default, the view more link has a label of 'View More'.
+      viewMoreLabel: defaultConfigOption(
+        config,
+        ['viewMoreLabel', 'viewAllText'],
+        TranslationFlagger.flag({
+          phrase: 'View More',
+          context: 'Button label, view more [results]'
+        })
+      ),
+      // Whether to show a result count.
+      showResultCount: false,
+      // Whether to use AccordionResults (DEPRECATED)
+      useAccordion: false,
+      // Override vertical config defaults with user given config.
+      ...config,
+      // Config for the applied filters bar. Must be placed after ...config to not override defaults.
+      appliedFilters: {
+        // Whether to display applied filters.
+        show: defaultConfigOption(config, ['appliedFilters.show', 'showAppliedFilters'], topLevelAppliedFilters.show),
+        // Whether to show field names, e.g. Location in Location: Virginia.
+        showFieldNames: defaultConfigOption(config,
+          ['appliedFilters.showFieldNames', 'showFieldNames'], topLevelAppliedFilters.showFieldNames),
+        // Hide filters with these field ids.
+        hiddenFields: defaultConfigOption(config,
+          ['appliedFilters.hiddenFields', 'hiddenFields'], topLevelAppliedFilters.hiddenFields),
+        // Symbol placed between the result count and the applied filters.
+        resultsCountSeparator: defaultConfigOption(config,
+          ['appliedFilters.resultsCountSeparator', 'resultsCountSeparator'], topLevelAppliedFilters.resultsCountSeparator),
+        // Whether to show a 'change filters' link, linking back to verticalURL.
+        showChangeFilters: defaultConfigOption(config,
+          ['appliedFilters.showChangeFilters', 'showChangeFilters'], topLevelAppliedFilters.showChangeFilters),
+        // The text for the change filters link.
+        changeFiltersText: defaultConfigOption(config,
+          ['appliedFilters.changeFiltersText', 'changeFiltersText'], topLevelAppliedFilters.changeFiltersText),
+        // The symbol placed between different filters with the same fieldName. e.g. Location: Virginia | New York | Miami.
+        delimiter: defaultConfigOption(config, ['appliedFilters.delimiter'], topLevelAppliedFilters.delimiter),
+        // The aria-label given to the applied filters bar.
+        labelText: defaultConfigOption(config, ['appliedFilters.labelText'], topLevelAppliedFilters.labelText)
+      }
+    };
   }
 }
 
@@ -76,13 +189,13 @@

Source: ui/components/results/universalresultscomponent.j
- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/ui_components_results_verticalresultscomponent.js.html b/docs/ui_components_results_verticalresultscomponent.js.html new file mode 100644 index 000000000..bc329eebe --- /dev/null +++ b/docs/ui_components_results_verticalresultscomponent.js.html @@ -0,0 +1,523 @@ + + + + + JSDoc: Source: ui/components/results/verticalresultscomponent.js + + + + + + + + + + +
+ +

Source: ui/components/results/verticalresultscomponent.js

+ + + + + + +
+
+
/** @module VerticalResultsComponent */
+
+import Component from '../component';
+
+import AlternativeVerticalsComponent from './alternativeverticalscomponent';
+import MapComponent from '../map/mapcomponent';
+import ResultsContext from '../../../core/storage/resultscontext';
+import StorageKeys from '../../../core/storage/storagekeys';
+import SearchStates from '../../../core/storage/searchstates';
+import CardComponent from '../cards/cardcomponent';
+import ResultsHeaderComponent from './resultsheadercomponent';
+import { replaceUrlParams, filterParamsForExperienceLink } from '../../../core/utils/urlutils';
+import Icons from '../../icons/index';
+import { defaultConfigOption } from '../../../core/utils/configutils';
+import { getTabOrder } from '../../tools/taborder';
+import SearchParams from '../../dom/searchparams';
+import TranslationFlagger from '../../i18n/translationflagger';
+import { getContainerClass } from '../../../core/utils/resultsutils';
+
+class VerticalResultsConfig {
+  constructor (config = {}) {
+    Object.assign(this, config);
+
+    /**
+     * isUniversal is set to true if this component is added by the UniversalResultsComponent
+     * @type {boolean}
+     * @private
+     */
+    this.isUniversal = config.isUniversal || false;
+
+    const parentOpts = config._parentOpts || {};
+
+    /**
+     * Custom render function
+     * @type {function}
+     */
+    this.renderItem = config.renderItem || parentOpts.renderItem;
+
+    /**
+     * Custom item template
+     * @type {string}
+     */
+    this.itemTemplate = config.itemTemplate || parentOpts.itemTemplate;
+
+    /**
+     * The maximum number of columns to display, supports 1, 2, 3, or 4.
+     * @type {number}
+     */
+    this.maxNumberOfColumns = config.maxNumberOfColumns || 1;
+
+    /**
+     * The config to pass to the card
+     * @type {Object}
+     */
+    this.card = config.card || {};
+
+    /**
+     * Vertical URL for view more link
+     * @type {string}
+     */
+    this.verticalURL = config.verticalURL;
+
+    /**
+     * Whether to display the number of results.
+     * @type {boolean}
+     */
+    this.showResultCount = config.showResultCount === undefined ? true : config.showResultCount;
+
+    /**
+     * A custom results count template.
+     * @type {string}
+     */
+    this.resultsCountTemplate = config.resultsCountTemplate || '';
+
+    /**
+     * Whether to display the results header (assuming there is something like the results count
+     * or applied filters to display).
+     * @type {boolean}
+     */
+    this.hideResultsHeader = config.hideResultsHeader;
+
+    /**
+     * Config for the applied filters in the results header.
+     * @type {Object}
+     */
+    this.appliedFilters = {
+      /**
+       * If present, show the filters that were ultimately applied to this query
+       * @type {boolean}
+       */
+      show: defaultConfigOption(config, ['appliedFilters.show', 'showAppliedFilters'], true),
+
+      /**
+       * If showResultCount and showAppliedFilters are true,
+       * display this separator between the result count and the applied query filters
+       * @type {string}
+       */
+      resultsCountSeparator: defaultConfigOption(config, ['appliedFilters.resultsCountSeparator', 'resultsCountSeparator'], '|'),
+
+      /**
+       * If showAppliedFilters is true, show the field name in the string followed by a colon.
+       * @type {boolean}
+       */
+      showFieldNames: defaultConfigOption(config, ['appliedFilters.showFieldNames', 'showFieldNames'], false),
+
+      /**
+       * Any fieldIds in hiddenFields will be hidden from the list of appied filters.
+       * @type {Array<string>}
+       */
+      hiddenFields: defaultConfigOption(config, ['appliedFilters.hiddenFields', 'hiddenFields'], ['builtin.entityType']),
+
+      /**
+       * The character that should separate each field (and its associated filters) within the applied filter bar
+       * @type {string}
+       */
+      delimiter: defaultConfigOption(config, ['appliedFilters.delimiter'], '|'),
+
+      /**
+       * If the filters are shown, whether or not they should be removable from within the applied filter bar.
+       * @type {boolean}
+       */
+      removable: defaultConfigOption(config, ['appliedFilters.removable'], false),
+
+      /**
+       * Whether to show the change filters link on universal results.
+       * @type {boolean}
+       **/
+      showChangeFilters: defaultConfigOption(config, ['appliedFilters.showChangeFilters', 'showChangeFilters'], false),
+
+      /**
+       * The text for the change filters link.
+       * @type {string}
+       */
+      changeFiltersText: defaultConfigOption(config, ['appliedFilters.changeFiltersText', 'changeFiltersText']),
+
+      /**
+       * The aria-label given to the applied filters bar. Defaults to 'Filters applied to this search:'.
+       * @type {string}
+       **/
+      labelText: defaultConfigOption(
+        config,
+        ['appliedFilters.labelText'],
+        TranslationFlagger.flag({
+          phrase: 'Filters applied to this search:'
+        })
+      ),
+
+      /**
+       * The aria-label given to the removable filter buttons.
+       * @type {string}
+       */
+      removableLabelText: defaultConfigOption(
+        config,
+        ['appliedFilters.removableLabelText'],
+        TranslationFlagger.flag({
+          phrase: 'Remove this filter',
+          context: 'Button label'
+        })
+      )
+    };
+
+    /**
+     * Text for the view more button.
+     * @type {string}
+     */
+    this.viewMoreLabel = defaultConfigOption(
+      config,
+      ['viewMoreLabel', 'viewAllText'],
+      TranslationFlagger.flag({
+        phrase: 'View More',
+        context: 'Button label, view more [results]'
+      })
+    );
+  }
+}
+
+export default class VerticalResultsComponent extends Component {
+  constructor (config = {}, systemConfig = {}) {
+    super(new VerticalResultsConfig(APPLY_SYNONYMS(config)), systemConfig);
+
+    const noResultsConfig = this._config.noResults ||
+      this.core.storage.get(StorageKeys.NO_RESULTS_CONFIG);
+    /**
+     * A parsed version of the noResults config provided to the component.
+     * Applies sensible defaults if certain values are not set.
+     * @type {Object}
+     * @private
+     */
+    this._noResultsConfig = Object.assign(
+      { displayAllResults: false, template: '' }, noResultsConfig);
+
+    /**
+     * Boolean indicating if legacy no results display should be used.
+     * @type {boolean}
+     * @private
+     */
+    this._useLegacyNoResults = this._config.isUniversal || !noResultsConfig;
+
+    /**
+     * _displayAllResults controls if all results for the vertical will display
+     * when there are no results for a query.
+     * @type {boolean}
+     * @private
+     */
+    this._displayAllResults = this._noResultsConfig.displayAllResults;
+
+    /**
+     * Specifies a custom no results template.
+     *
+     * @type {string}
+     * @private
+     */
+    this._noResultsTemplate = this._noResultsConfig.template;
+
+    this.core.storage.registerListener({
+      eventType: 'update',
+      storageKey: StorageKeys.VERTICAL_RESULTS,
+      callback: results => {
+        this.updateContainerClass(results.searchState);
+        if (results.searchState === SearchStates.SEARCH_COMPLETE) {
+          this.setState(results);
+        }
+      }
+    });
+
+    /**
+     * Vertical config from config, if not present, fall back to global verticalPagesConfig
+     * @type {Array.<object>}
+     * @private
+     */
+    this._verticalsConfig = config.verticalPages || this.core.storage
+      .get(StorageKeys.VERTICAL_PAGES_CONFIG)
+      .get() || [];
+    /**
+     * @type {Array<Result>}
+     */
+    this.results = [];
+    this.numColumns = this._config.maxNumberOfColumns;
+
+    /**
+     * Config options used in the {@link ResultsHeaderComponent}
+     */
+    this.resultsHeaderOpts = {
+      showFieldNames: this._config.appliedFilters.showFieldNames,
+      resultsCountSeparator: this._config.appliedFilters.resultsCountSeparator,
+      showAppliedFilters: this._config.appliedFilters.show,
+      showChangeFilters: this._config.appliedFilters.showChangeFilters,
+      changeFiltersText: this._config.appliedFilters.changeFiltersText,
+      showResultCount: this._config.showResultCount,
+      removable: this._config.appliedFilters.removable,
+      delimiter: this._config.appliedFilters.delimiter,
+      labelText: this._config.appliedFilters.labelText,
+      removableLabelText: this._config.appliedFilters.removableLabelText,
+      hiddenFields: this._config.appliedFilters.hiddenFields,
+      resultsCountTemplate: this._config.resultsCountTemplate
+    };
+  }
+
+  onCreate () {
+    this.updateContainerClass(SearchStates.PRE_SEARCH);
+  }
+
+  mount () {
+    if (Object.keys(this.getState()).length > 0) {
+      super.mount();
+    }
+    return this;
+  }
+
+  static areDuplicateNamesAllowed () {
+    return true;
+  }
+
+  getBaseUniversalUrl () {
+    const universalConfig = this._verticalsConfig.find(config => !config.verticalKey) || {};
+    return universalConfig.url;
+  }
+
+  getUniversalUrl () {
+    const baseUniversalUrl = this.getBaseUniversalUrl();
+    if (!baseUniversalUrl) {
+      return undefined;
+    }
+    return this._getExperienceURL(
+      baseUniversalUrl,
+      new SearchParams(this.core.storage.getCurrentStateUrlMerged())
+    );
+  }
+
+  getVerticalURL (data = {}) {
+    const verticalConfig = this._verticalsConfig.find(
+      config => config.verticalKey === this.verticalKey
+    ) || {};
+    const verticalURL = this._config.verticalURL || verticalConfig.url ||
+      data.verticalURL || this.verticalKey + '.html';
+
+    const navigationData = this.core.storage.get(StorageKeys.NAVIGATION);
+    const dataTabOrder = navigationData ? navigationData.tabOrder : [];
+    const tabOrder = getTabOrder(
+      this._verticalsConfig, dataTabOrder, this.core.storage.getCurrentStateUrlMerged());
+    const params = new SearchParams(this.core.storage.getCurrentStateUrlMerged());
+    params.set('tabOrder', tabOrder);
+
+    return this._getExperienceURL(verticalURL, params);
+  }
+
+  /**
+   * Adds parameters that are dynamically set. Removes parameters for facets,
+   * filters, and pagination, which should not persist across the experience.
+   * @param {string} baseUrl The url append the appropriate params to. Note:
+   *    params already on the baseUrl will be stripped
+   * @param {SearchParams} params The parameters to include in the experience URL
+   * @return {string} The formatted experience URL with appropriate query params
+   */
+  _getExperienceURL (baseUrl, params) {
+    params.set(StorageKeys.QUERY, this.query);
+
+    const context = this.core.storage.get(StorageKeys.API_CONTEXT);
+    if (context) {
+      params.set(StorageKeys.API_CONTEXT, context);
+    }
+    const referrerPageUrl = this.core.storage.get(StorageKeys.REFERRER_PAGE_URL);
+    if (referrerPageUrl !== undefined) {
+      params.set(StorageKeys.REFERRER_PAGE_URL, referrerPageUrl);
+    }
+
+    const sessionsOptIn = this.core.storage.get(StorageKeys.SESSIONS_OPT_IN);
+    if (sessionsOptIn && sessionsOptIn.setDynamically) {
+      params.set(StorageKeys.SESSIONS_OPT_IN, sessionsOptIn.value);
+    }
+
+    const filteredParams = filterParamsForExperienceLink(
+      params,
+      types => this.componentManager.getComponentNamesForComponentTypes(types)
+    );
+
+    return replaceUrlParams(baseUrl, filteredParams);
+  }
+
+  /**
+   * Updates the search state css class on this component's container.
+   */
+  updateContainerClass (searchState) {
+    Object.values(SearchStates).forEach(searchState => {
+      this.removeContainerClass(getContainerClass(searchState));
+    });
+
+    this.addContainerClass(getContainerClass(searchState));
+  }
+
+  setState (data = {}, val) {
+    /**
+     * @type {Array<Result>}
+     */
+    this.results = data.results || [];
+    this.resultsCount = data.resultsCount;
+    this.verticalKey = data.verticalConfigId;
+    this.resultsContext = data.resultsContext;
+    const searchState = data.searchState || SearchStates.PRE_SEARCH;
+    const displayResultsIfExist = this._config.isUniversal ||
+      this._displayAllResults ||
+      data.resultsContext === ResultsContext.NORMAL;
+    this.query = this.core.storage.get(StorageKeys.QUERY);
+    return super.setState(Object.assign({ results: [] }, data, {
+      searchState: searchState,
+      isPreSearch: searchState === SearchStates.PRE_SEARCH,
+      isSearchLoading: searchState === SearchStates.SEARCH_LOADING,
+      isSearchComplete: searchState === SearchStates.SEARCH_COMPLETE,
+      eventOptions: this.eventOptions(),
+      universalUrl: this.getUniversalUrl(),
+      verticalURL: this.getVerticalURL(data),
+      query: this.query,
+      currentVerticalLabel: this._currentVerticalLabel,
+      resultsPresent: displayResultsIfExist && this.results.length !== 0,
+      showNoResults: this.resultsContext === ResultsContext.NO_RESULTS,
+      placeholders: new Array(this._config.maxNumberOfColumns - 1),
+      numColumns: Math.min(this._config.maxNumberOfColumns, this.results.length),
+      useLegacyNoResults: this._useLegacyNoResults,
+      iconIsBuiltIn: Icons[this._config.icon],
+      nlpFilters: data.appliedQueryFilters || []
+    }), val);
+  }
+
+  /**
+   * helper to construct the eventOptions object for the view all link
+   * @returns {string}
+   */
+  eventOptions () {
+    return JSON.stringify({
+      verticalConfigId: this.verticalKey
+    });
+  }
+
+  static get type () {
+    return 'VerticalResults';
+  }
+
+  /**
+   * The template to render
+   * @returns {string}
+   * @override
+   */
+  static defaultTemplateName (config) {
+    return 'results/verticalresults';
+  }
+
+  addChild (data, type, opts) {
+    if (type === MapComponent.type) {
+      const _opts = {
+        noResults: this._noResultsConfig,
+        ...this._config.mapConfig,
+        ...opts
+      };
+      const _data = {
+        resultsContext: this.getState('resultsContext'),
+        map: data
+      };
+      return super.addChild(_data, type, _opts);
+    } else if (type === CardComponent.type) {
+      const updatedData = {
+        result: this.results[opts._index],
+        verticalKey: this.verticalKey
+      };
+      const newOpts = {
+        target: this._config.target,
+        ...this._config.card,
+        isUniversal: this._config.isUniversal,
+        template: this._config.itemTemplate,
+        render: this._config.renderItem,
+        modifier: this._config.modifier,
+        ...opts
+      };
+      return super.addChild(updatedData, type, newOpts);
+    } else if (type === AlternativeVerticalsComponent.type) {
+      const hasResults = this.results && this.results.length > 0;
+      data = this.core.storage.get(StorageKeys.ALTERNATIVE_VERTICALS);
+      const newOpts = {
+        template: this._noResultsTemplate,
+        baseUniversalUrl: this.getBaseUniversalUrl(),
+        verticalsConfig: this._verticalsConfig,
+        isShowingResults: this._displayAllResults && hasResults,
+        ...opts
+      };
+      return super.addChild(data, type, newOpts);
+    } else if (type === ResultsHeaderComponent.type) {
+      const resultsHeaderData = {
+        resultsLength: this.results.length,
+        resultsCount: this.resultsCount,
+        nlpFilters: this.getState('nlpFilters'),
+        ...data
+      };
+      const _opts = { ...opts };
+      if (this.resultsContext === ResultsContext.NO_RESULTS) {
+        _opts.showAppliedFilters = false;
+      }
+      return super.addChild(resultsHeaderData, type, {
+        isUniversal: this._config.isUniversal,
+        verticalURL: this.getVerticalURL(),
+        verticalKey: this.verticalKey,
+        ...this.resultsHeaderOpts,
+        ..._opts
+      });
+    }
+    return super.addChild(data, type, opts);
+  }
+}
+
+const APPLY_SYNONYMS = (config) => ({
+  icon: config.sectionTitleIconName || config.sectionTitleIconUrl,
+  title: config.sectionTitle,
+  ...config
+});
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/ui_components_results_verticalresultscountcomponent.js.html b/docs/ui_components_results_verticalresultscountcomponent.js.html new file mode 100644 index 000000000..8517ac2bf --- /dev/null +++ b/docs/ui_components_results_verticalresultscountcomponent.js.html @@ -0,0 +1,123 @@ + + + + + JSDoc: Source: ui/components/results/verticalresultscountcomponent.js + + + + + + + + + + +
+ +

Source: ui/components/results/verticalresultscountcomponent.js

+ + + + + + +
+
+
import Component from '../component';
+import StorageKeys from '../../../core/storage/storagekeys';
+import SearchStates from '../../../core/storage/searchstates';
+import ResultsContext from '../../../core/storage/resultscontext';
+
+export default class VerticalResultsCountComponent extends Component {
+  constructor (config = {}, systemConfig = {}) {
+    super(config, systemConfig);
+
+    this.core.storage.registerListener({
+      eventType: 'update',
+      storageKey: StorageKeys.VERTICAL_RESULTS,
+      callback: results => {
+        if (results.searchState === SearchStates.SEARCH_COMPLETE) {
+          this.setState(results);
+        }
+      }
+    });
+
+    /**
+     * When the page is in a No Results state, whether to display the
+     * vertical results count.
+     * @type {boolean}
+     */
+    this._visibleForNoResults = !!(config.noResults || {}).visible;
+  }
+
+  static areDuplicateNamesAllowed () {
+    return true;
+  }
+
+  setState (data) {
+    const verticalResults = data || {};
+
+    /**
+     * Total number of results.
+     * @type {number}
+     */
+    const resultsCount = verticalResults.resultsCount || 0;
+
+    /**
+     * Number of results displayed on the page.
+     * @type {number}
+     */
+    const resultsLength = (verticalResults.results || []).length;
+
+    const offset = this.core.storage.get(StorageKeys.SEARCH_OFFSET) || 0;
+    const isNoResults = verticalResults.resultsContext === ResultsContext.NO_RESULTS;
+    const hasZeroResults = resultsCount === 0;
+    const isHidden = (!this._visibleForNoResults && isNoResults) || hasZeroResults;
+    return super.setState({
+      ...data,
+      total: resultsCount,
+      pageStart: offset + 1,
+      pageEnd: offset + resultsLength,
+      isHidden: isHidden
+    });
+  }
+
+  static get type () {
+    return 'VerticalResultsCount';
+  }
+
+  /**
+   * The template to render
+   * @returns {string}
+   * @override
+   */
+  static defaultTemplateName (config) {
+    return 'results/verticalresultscount';
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/ui_components_search_autocompletecomponent.js.html b/docs/ui_components_search_autocompletecomponent.js.html index ea9bf6b66..eb0d90999 100644 --- a/docs/ui_components_search_autocompletecomponent.js.html +++ b/docs/ui_components_search_autocompletecomponent.js.html @@ -53,8 +53,8 @@

Source: ui/components/search/autocompletecomponent.js

Source: ui/components/search/autocompletecomponent.js

Source: ui/components/search/autocompletecomponent.js

Source: ui/components/search/autocompletecomponent.js

Source: ui/components/search/autocompletecomponent.js

Source: ui/components/search/autocompletecomponent.js { + // TODO(jdelerme): Close logic to be moved to parent + DOM.on(document, 'click', e => { + if (DOM.matches(e.target, '.js-yxt-AutoComplete-wrapper *') || DOM.matches(e.target, this._inputEl)) { + return; + } this.close(); }); @@ -187,8 +268,14 @@

Source: ui/components/search/autocompletecomponent.js

{ + this.autoComplete(queryInput.value); + }); + } + // Allow the user to select a result with the mouse - DOM.delegate(this._container, '.js-yext-autocomplete-option', 'mousedown', (evt, target) => { + DOM.delegate(this._container, '.js-yext-autocomplete-option', 'click', (evt, target) => { let data = target.dataset; let val = data.short; @@ -237,7 +324,7 @@

Source: ui/components/search/autocompletecomponent.js

Source: ui/components/search/autocompletecomponent.jsSource: ui/components/search/autocompletecomponent.jsSource: ui/components/search/autocompletecomponent.js 0) { + return true; + } + } + + return false; + } + handleNavigateResults (key, e) { let sections = this._state.get('sections'); if (sections === undefined || sections.length <= 0) { return; } + // Tabbing out or enter should close the auto complete. + if (key === Keys.TAB) { + this.close(); + return; + } + let results = sections[this._sectionIndex].results; if (key === Keys.UP) { e.preventDefault(); @@ -339,12 +463,20 @@

Source: ui/components/search/autocompletecomponent.js

= 0 && this._resultIndex >= 0) { filter = JSON.stringify(sections[this._sectionIndex].results[this._resultIndex].filter); @@ -353,8 +485,9 @@

Source: ui/components/search/autocompletecomponent.js

Source: ui/components/search/autocompletecomponent.js
- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/ui_components_search_filterpickercomponent.js.html b/docs/ui_components_search_filterpickercomponent.js.html deleted file mode 100644 index c26a6ef8b..000000000 --- a/docs/ui_components_search_filterpickercomponent.js.html +++ /dev/null @@ -1,248 +0,0 @@ - - - - - JSDoc: Source: ui/components/search/filterpickercomponent.js - - - - - - - - - - -
- -

Source: ui/components/search/filterpickercomponent.js

- - - - - - -
-
-
/** @module FilterPickerComponent */
-
-import Component from '../component';
-import DOM from '../../dom/dom';
-import * as StorageKeys from '../../../core/storage/storagekeys';
-import Filter from '../../../core/models/filter';
-
-export default class FilterPickerComponent extends Component {
-  constructor (opts = {}) {
-    super(opts);
-
-    /**
-     * The template name to use for rendering with handlebars
-     * @type {string}
-     */
-    this._templateName = 'search/search';
-
-    /**
-     * The input key for the vertical search configuration
-     * @type {string}
-     */
-    this._barKey = opts.barKey || opts.inputKey || null;
-
-    /**
-     * The vertical key for vertical search configuration
-     * @type {string}
-     */
-    this._verticalKey = opts.verticalKey || null;
-
-    /**
-     * Query submission is based on a form as context.
-     * Optionally provided, otherwise defaults to native form node within container
-     * @type {string} CSS selector
-     */
-    this._formEl = opts.formSelector || 'form';
-
-    /**
-     * The input element used for searching and wires up the keyboard interaction
-     * Optionally provided.
-     * @type {string} CSS selector
-     */
-    this._inputEl = opts.inputEl || '.js-yext-query';
-
-    /**
-     * The title used, provided to the template as a data point
-     * Optionally provided.
-     * @type {string}
-     */
-    this.title = opts.title;
-
-    /**
-     * The search text used for labeling the input box, also provided to template.
-     * Optionally provided
-     * @type {string}
-     */
-    this.searchText = opts.searchText || 'What are you interested in?';
-
-    /**
-     * The query text to show as the first item for auto complete.
-     * Optionally provided
-     * @type {string}
-     */
-    this.promptHeader = opts.promptHeader || null;
-
-    /**
-     * Auto focuses the input box if set to true.
-     * Optionally provided, defaults to false.
-     * @type {boolean}
-     */
-    this.autoFocus = opts.autoFocus === true;
-
-    /**
-     * submitURL will force the search query submission to get
-     * redirected to the URL provided.
-     * Optional, defaults to null.
-     *
-     * If no redirectUrl provided, we keep the page as a single page app.
-     *
-     * @type {boolean}
-     */
-    this.redirectUrl = opts.redirectUrl || null;
-
-    /**
-     * The query string to use for the input box, provided to template for rendering.
-     * Optionally provided
-     * @type {string}
-     */
-    this.query = opts.query || this.getUrlParams().get(`${this.name}.query`) || '';
-
-    /**
-     * The filter string to use for the provided query
-     * Optionally provided
-     * @type {string}
-     */
-    this.filter = opts.filter || this.getUrlParams().get(`${this.name}.filter`) || '';
-  }
-
-  static get type () {
-    return 'FilterPicker';
-  }
-
-  onCreate () {
-    if (this.query && this.query.length > 0 && this.filter && this.filter.length > 0) {
-      this.search(this.query, this.filter);
-    }
-
-    this.bindBrowserHistory();
-  }
-
-  onMount () {
-    // Wire up our search handling and auto complete
-    this.initAutoComplete(this._inputEl);
-
-    if (this.autoFocus === true && this.query.length === 0) {
-      DOM.query(this._container, this._inputEl).focus();
-    }
-  }
-
-  /**
-   * A helper method to wire up our auto complete on an input selector
-   * @param {string} inputSelector CSS selector to bind our auto complete component to
-   */
-  initAutoComplete (inputSelector) {
-    this._inputEl = inputSelector;
-
-    this.componentManager.create('AutoComplete', {
-      parent: this,
-      name: `${this.name}.autocomplete`,
-      isFilterSearch: true,
-      container: '.yxt-SearchBar-autocomplete',
-      promptHeader: this.promptHeader,
-      originalQuery: this.query,
-      originalFilter: this.filter,
-      inputEl: inputSelector,
-      verticalKey: this._verticalKey,
-      barKey: this._barKey,
-      onSubmit: (query, filter) => {
-        this.search(query, filter);
-      }
-    });
-  }
-
-  search (query, filter) {
-    const params = this.getUrlParams();
-
-    params.set(`${this.name}.query`, query);
-    params.set(`${this.name}.filter`, filter);
-
-    // If we have a redirectUrl, we want the params to be
-    // serialized and submitted.
-    if (typeof this.redirectUrl === 'string') {
-      window.location.href = this.redirectUrl + '?' + params.toString();
-      return false;
-    }
-
-    window.history.pushState({
-      query: query,
-      filter: filter
-    }, query, '?' + params.toString());
-
-    this.core.setFilter(this.name, filter);
-    const filters = this.core.storage.getAll(StorageKeys.FILTER);
-    let totalFilter = filters[0];
-    if (filters.length > 1) {
-      totalFilter = Filter.and(...filters);
-    }
-
-    this.core.verticalSearch('', this._verticalKey, JSON.stringify(totalFilter));
-  }
-
-  setState (data) {
-    return super.setState(Object.assign({
-      title: this.title,
-      searchText: this.searchText,
-      query: this.query,
-      filter: this.filter
-    }, data));
-  }
-
-  getUrlParams (url) {
-    url = url || window.location.search.substring(1);
-    return new URLSearchParams(url);
-  }
-
-  bindBrowserHistory () {
-    DOM.on(window, 'popstate', () => {
-      this.query = this.getUrlParams().get(`${this.name}.query`);
-      this.filter = this.getUrlParams().get(`${this.name}.filter`);
-      this.setState({
-        query: this.query,
-        filter: this.filter
-      });
-
-      this.search(this.query, this.filter);
-    });
-  }
-}
-
-
-
- - - - -
- - - -
- -
- Documentation generated by JSDoc 3.6.2 on Wed Jul 03 2019 10:34:41 GMT-0400 (Eastern Daylight Time) -
- - - - - diff --git a/docs/ui_components_search_filtersearchcomponent.js.html b/docs/ui_components_search_filtersearchcomponent.js.html index c8d8854be..8e35d5952 100644 --- a/docs/ui_components_search_filtersearchcomponent.js.html +++ b/docs/ui_components_search_filtersearchcomponent.js.html @@ -19,11 +19,11 @@

Source: ui/components/search/filtersearchcomponent.js

+ - - +
/** @module FilterSearchComponent */
@@ -32,6 +32,12 @@ 

Source: ui/components/search/filtersearchcomponent.js

Source: ui/components/search/filtersearchcomponent.jsSource: ui/components/search/filtersearchcomponent.js { + const query = data.get(`${StorageKeys.QUERY}.${this.name}`) || ''; + const filter = data.get(`${StorageKeys.FILTER}.${this.name}`); + if (filter && query) { + this.query = query; + this.filter = JSON.parse(filter); + this._saveFilterNodeToStorage(); + } else { + this._removeFilterNode(); + } + this.setState(); + } + }); /** * The filter string to use for the provided query * Optionally provided * @type {string} */ - this.filter = opts.filter || this.getUrlParams().get(`${this.name}.filter`) || ''; + this.filter = config.filter || this.core.storage.get(`${StorageKeys.FILTER}.${this.name}`); + if (typeof this.filter === 'string') { + try { + this.filter = JSON.parse(this.filter); + } catch (e) {} + } + + if (this.query && this.filter) { + this._saveFilterNodeToStorage(); + } + + this.searchParameters = buildSearchParameters(config.searchParameters); } static get type () { - return 'FilterSearch'; + return ComponentTypes.FILTER_SEARCH; } - onCreate () { - if (this.query && this.query.length > 0 && this.filter && this.filter.length > 0) { - const params = this.getUrlParams(); - params.set(`${this.name}.query`, this.query); - params.set(`${this.name}.filter`, this.filter); - window.history.pushState({}, '', '?' + params.toString()); - this.core.setFilter(this.name, Filter.fromResponse(this.filter)); - this.search(); - } - - this.bindBrowserHistory(); + /** + * The template to render + * @returns {string} + * @override + */ + static defaultTemplateName () { + return 'search/filtersearch'; } onMount () { + if (this.autoCompleteComponent) { + this.autoCompleteComponent.remove(); + } // Wire up our search handling and auto complete this.initAutoComplete(this._inputEl); @@ -156,6 +185,29 @@

Source: ui/components/search/filtersearchcomponent.js

this._removeFilterNode() + }); + } + /** * A helper method to wire up our auto complete on an input selector * @param {string} inputSelector CSS selector to bind our auto complete component to @@ -163,19 +215,21 @@

Source: ui/components/search/filtersearchcomponent.js

{ - const params = this.getUrlParams(); + this.filter = Filter.fromResponse(filter); + const filterNode = this._buildFilterNode(query, this.filter); + + const params = new SearchParams(this.core.storage.getCurrentStateUrlMerged()); params.set(`${this.name}.query`, query); params.set(`${this.name}.filter`, filter); @@ -186,57 +240,35 @@

Source: ui/components/search/filtersearchcomponent.js

1) { - totalFilter = Filter.and(...filters); + search (queryTrigger) { + if (this._storeOnChange) { + return; } - const searchQuery = this.core.storage.getState(StorageKeys.QUERY) || ''; - - this.core.verticalSearch(searchQuery, this._verticalKey, JSON.stringify(totalFilter)); + this.core.triggerSearch(queryTrigger); } setState (data) { return super.setState(Object.assign({ title: this.title, searchText: this.searchText, - query: this.query, - filter: this.filter + query: this.query }, data)); } - - getUrlParams (url) { - url = url || window.location.search.substring(1); - return new URLSearchParams(url); - } - - bindBrowserHistory () { - DOM.on(window, 'popstate', () => { - this.query = this.getUrlParams().get(`${this.name}.query`); - this.filter = this.getUrlParams().get(`${this.name}.filter`); - this.setState({ - query: this.query, - filter: this.filter - }); - - this._saveQueryAndFilter(this.query, this.filter); - this.search(); - }); - } }
@@ -248,13 +280,13 @@

Source: ui/components/search/filtersearchcomponent.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/ui_components_search_locationbiascomponent.js.html b/docs/ui_components_search_locationbiascomponent.js.html new file mode 100644 index 000000000..098a9abd6 --- /dev/null +++ b/docs/ui_components_search_locationbiascomponent.js.html @@ -0,0 +1,238 @@ + + + + + JSDoc: Source: ui/components/search/locationbiascomponent.js + + + + + + + + + + +
+ +

Source: ui/components/search/locationbiascomponent.js

+ + + + + + +
+
+
import Component from '../component';
+import StorageKeys from '../../../core/storage/storagekeys';
+import DOM from '../../dom/dom';
+import TranslationFlagger from '../../i18n/translationflagger';
+import isEqual from 'lodash.isequal';
+import SearchStates from '../../../core/storage/searchstates';
+
+const DEFAULT_CONFIG = {
+  ipAccuracyHelpText: TranslationFlagger.flag({
+    phrase: 'based on your internet address',
+    context: 'Describes the accuracy of estimated location. Example: Salt Lake City, Utah based on your internet address'
+  }),
+  deviceAccuracyHelpText: TranslationFlagger.flag({
+    phrase: 'based on your device',
+    context: 'Describes the accuracy of estimated location. Example: Salt Lake City, Utah based on your device'
+  }),
+  updateLocationButtonText: TranslationFlagger.flag({
+    phrase: 'Update your location',
+    context: 'Button label'
+  })
+};
+
+/**
+ * LocationBiasComponent will show the user where is used for location bias and allow user to
+ * improve accuracy with HTML5 geolocation.
+ *
+ * @extends Component
+ */
+export default class LocationBiasComponent extends Component {
+  constructor (config = {}, systemConfig = {}) {
+    super({ ...DEFAULT_CONFIG, ...config }, systemConfig);
+
+    /**
+     * Recieve updates from storage based on this index
+     * @type {StorageKey}
+     */
+    this.core.storage.registerListener({
+      storageKey: StorageKeys.LOCATION_BIAS,
+      eventType: 'update',
+      callback: data => {
+        const searchIsLoading = data.searchState === SearchStates.SEARCH_LOADING;
+        if (!searchIsLoading && !isEqual(data, this.getState('locationBias'))) {
+          this.setState(data);
+        }
+      }
+    });
+
+    /**
+     * The optional vertical key for vertical search configuration
+     * If not provided, when location updated,
+     * a universal search will be triggered.
+     * @type {string}
+     */
+    // TODO: Remove config.verticalKey
+    this._verticalKey = config.verticalKey || this.core.storage.get(StorageKeys.SEARCH_CONFIG).verticalKey || null;
+
+    /**
+     * The element used for updating location
+     * Optionally provided.
+     * @type {string} CSS selector
+     */
+    this._updateLocationEl = config.updateLocationEl || '.js-locationBias-update-location';
+
+    this._locationDisplayName = '';
+
+    this._accuracy = '';
+
+    this._allowUpdate = true;
+
+    /**
+     * Options to pass to the geolocation api.
+     * @type {Object}
+     */
+    this._geolocationOptions = {
+      enableHighAccuracy: false,
+      timeout: 6000,
+      maximumAge: 300000,
+      ...config.geolocationOptions
+    };
+
+    /**
+     * Options for the geolocation timeout alert.
+     * @type {Object}
+     */
+    this._geolocationTimeoutAlert = {
+      enabled: false,
+      message: TranslationFlagger.flag({
+        phrase: 'We are unable to determine your location'
+      }),
+      ...config.geolocationTimeoutAlert
+    };
+  }
+
+  static get type () {
+    return 'LocationBias';
+  }
+
+  static defaultTemplateName () {
+    return 'search/locationbias';
+  }
+
+  onMount () {
+    if (!this._allowUpdate) {
+      return;
+    }
+    this._disableLocationUpdateIfGeolocationDenied();
+    DOM.on(this._updateLocationEl, 'click', (e) => {
+      if ('geolocation' in navigator) {
+        navigator.geolocation.getCurrentPosition((position) => {
+          this.core.storage.set(StorageKeys.GEOLOCATION, {
+            lat: position.coords.latitude,
+            lng: position.coords.longitude,
+            radius: position.coords.accuracy
+          });
+          this.core.triggerSearch();
+        },
+        (err) => this._handleGeolocationError(err),
+        this._geolocationOptions);
+      }
+      // TODO: Should we throw error or warning here if no geolocation?
+    });
+  }
+
+  _handleGeolocationError (err) {
+    if (err.code === 1) {
+      this._disableLocationUpdate();
+    }
+    const { enabled, message } = this._geolocationTimeoutAlert;
+    if (enabled) {
+      window.alert(message);
+    }
+  }
+
+  setState (data, val) {
+    this._locationDisplayName = data.locationDisplayName;
+    this._accuracy = data.accuracy;
+    return super.setState(Object.assign({}, data, {
+      locationDisplayName: this._getLocationDisplayName(data),
+      accuracyText: this._getAccuracyHelpText(data.accuracy),
+      isPreciseLocation: data.accuracy === 'DEVICE' && this._allowUpdate,
+      isUnknownLocation: data.accuracy === 'UNKNOWN',
+      shouldShow: data.accuracy !== undefined && data.accuracy !== null,
+      allowUpdate: this._allowUpdate,
+      locationBias: data
+    }, val));
+  }
+
+  _getLocationDisplayName (data) {
+    if (data.accuracy === 'UNKNOWN') {
+      return TranslationFlagger.flag({
+        phrase: 'Unknown Location'
+      });
+    }
+    return data.locationDisplayName;
+  }
+
+  _getAccuracyHelpText (accuracy) {
+    switch (accuracy) {
+      case 'IP':
+        return this._config.ipAccuracyHelpText;
+      case 'DEVICE':
+        return this._config.deviceAccuracyHelpText;
+      default:
+        return '';
+    }
+  }
+
+  _disableLocationUpdateIfGeolocationDenied () {
+    if ('permissions' in navigator) {
+      navigator.permissions.query({ name: 'geolocation' })
+        .then((result) => {
+          if (result.state === 'denied') {
+            this._disableLocationUpdate();
+          }
+        });
+    }
+  }
+
+  _disableLocationUpdate () {
+    this.core.storage.delete(StorageKeys.GEOLOCATION);
+    this._allowUpdate = false;
+    this.setState({
+      locationDisplayName: this._locationDisplayName,
+      accuracy: this._accuracy
+    });
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/ui_components_search_searchcomponent.js.html b/docs/ui_components_search_searchcomponent.js.html index 82ec1d52f..130e392b3 100644 --- a/docs/ui_components_search_searchcomponent.js.html +++ b/docs/ui_components_search_searchcomponent.js.html @@ -19,19 +19,27 @@

Source: ui/components/search/searchcomponent.js

+ - - +
/** @module SearchComponent */
 
 import Component from '../component';
 import DOM from '../../dom/dom';
-import Filter from '../../../core/models/filter';
 import StorageKeys from '../../../core/storage/storagekeys';
+import SearchParams from '../../dom/searchparams';
+import TranslationFlagger from '../../i18n/translationflagger';
+import QueryUpdateListener from '../../../core/statelisteners/queryupdatelistener';
+import QueryTriggers from '../../../core/models/querytriggers';
+
+const IconState = {
+  'YEXT': 0,
+  'MAGNIFYING_GLASS': 1
+};
 
 /**
  * SearchComponent exposes an interface in order to create
@@ -40,70 +48,107 @@ 

Source: ui/components/search/searchcomponent.js

* @extends Component */ export default class SearchComponent extends Component { - constructor (opts = {}) { - super(opts); - - /** - * The template name to use for rendering with handlebars - * @type {string} - */ - this._templateName = 'search/search'; + constructor (config = {}, systemConfig = {}) { + super(config, systemConfig); /** - * The optional input key for the vertical search configuration + * The optional vertical key for vertical search configuration * If not provided, auto-complete and search will be based on universal * @type {string} */ - this._barKey = opts.barKey || null; + this._verticalKey = config.verticalKey || null; /** - * The optional vertical key for vertical search configuration - * If not provided, auto-complete and search will be based on universal - * @type {string} + * Query submission can optionally be based on a form as context. Note that if + * a form is not used, the component has no guarantee of WCAG compliance. */ - this._verticalKey = opts.verticalKey || null; + this._useForm = config.useForm !== undefined ? config.useForm : true; /** * Query submission is based on a form as context. * Optionally provided, otherwise defaults to native form node within container * @type {string} CSS selector */ - this._formEl = opts.formSelector || 'form'; + this._formEl = config.formSelector || 'form'; /** * The input element used for searching and wires up the keyboard interaction * Optionally provided. * @type {string} CSS selector */ - this._inputEl = opts.inputEl || '.js-yext-query'; + this._inputEl = config.inputEl || '.js-yext-query'; /** * The title used, provided to the template as a data point - * Optionally provided. + * Optionally provided. If not provided, no title will be included. * @type {string} */ - this.title = opts.title || 'Answers Universal Search'; + this.title = config.title; /** - * The search text used for labeling the input box, also provided to template. + * The label text is used for labeling the input box, also provided to template. * Optionally provided * @type {string} */ - this.searchText = opts.searchText || 'What are you interested in?'; + this.labelText = config.labelText || TranslationFlagger.flag({ + phrase: 'Conduct a search', + context: 'Labels an input field' + }); + + /** + * The submit text is used for labeling the submit button, also provided to the template. + * @type {string} + */ + this.submitText = config.submitText || TranslationFlagger.flag({ + phrase: 'Submit', + context: 'Button label' + }); + + /** + * The clear text is used for labeling the clear button, also provided to the template. + * @type {string} + */ + this.clearText = config.clearText || TranslationFlagger.flag({ + phrase: 'Clear', + context: 'Verb, clears search' + }); + + /** + * The submit icon is an icon for the submit button, if provided it will be displayed and the + * submit text will be used for screen readers. + * @type {string|null} + */ + this.submitIcon = config.submitIcon || null; /** * The query text to show as the first item for auto complete. * Optionally provided * @type {string} */ - this.promptHeader = opts.promptHeader || null; + this.promptHeader = config.promptHeader || null; /** * Auto focuses the input box if set to true. * Optionally provided, defaults to false. * @type {boolean} */ - this.autoFocus = opts.autoFocus === true; + this.autoFocus = config.autoFocus === true; + + /** + * If true, show an "x" that allows the user to clear the current + * query + * @type {boolean} + */ + this.clearButton = config.clearButton === undefined + ? true + : config.clearButton; + + /** + * When autofocusing on load, optionally open the autocomplete + * (preset prompts) + * @type {boolean} + */ + this.autocompleteOnLoad = config.autocompleteOnLoad || false; /** * submitURL will force the search query submission to get @@ -112,75 +157,418 @@

Source: ui/components/search/searchcomponent.js

* * If no redirectUrl provided, we keep the page as a single page app. * - * @type {boolean} + * @type {string} + */ + this.redirectUrl = config.redirectUrl || null; + + /** + * redirectUrlTarget will force the search query submission to open in the frame specified if + * redirectUrl is also supplied. + * Optional, defaults to current frame. + * + * @type {string} + */ + this.redirectUrlTarget = config.redirectUrlTarget || '_self'; + + /** + * true if there is another search bar present on the page. + * Twins only update the query, and do not search + */ + this._isTwin = config.isTwin; + + /** + * The search config from ANSWERS.init configuration */ - this.redirectUrl = opts.redirectUrl || null; + this._globalSearchConfig = this.core.storage.get(StorageKeys.SEARCH_CONFIG) || {}; + + /** + * The default initial search query, can be an empty string + */ + this._defaultInitialSearch = this._globalSearchConfig.defaultInitialSearch; /** * The query string to use for the input box, provided to template for rendering. * Optionally provided + * @type {string|null} + */ + this.query = config.query || this.core.storage.get(StorageKeys.QUERY); + this.core.storage.registerListener({ + eventType: 'update', + storageKey: StorageKeys.QUERY, + callback: q => { + this.query = q; + if (this.queryEl) { + this.queryEl.value = q; + } + if (q === null) { + return; + } + this._updateClearButtonVisibility(q); + } + }); + + /** + * The minimum time allowed in milliseconds between searches to prevent + * many duplicate searches back-to-back + * @type {number} + * @private + */ + this._searchCooldown = config.searchCooldown || 300; + + /** + * When true and "near me" intent is expressed, prompt the user for their geolocation + * @type {boolean} + * @private + */ + this._promptForLocation = config.promptForLocation === undefined + ? true + : Boolean(config.promptForLocation); + + /** + * Controls showing and hiding the search clear button + */ + this._showClearButton = this.clearButton && this.query; + + /** + * Whether or not to allow empty searches. + * @type {boolean} + * @private + */ + this._allowEmptySearch = !!config.allowEmptySearch; + + /** + * The name of the child AutoComplete component. * @type {string} + * @private */ - this.query = opts.query || this.getUrlParams().get('query') || ''; + this._autoCompleteName = `${this.name}.autocomplete`; + + /** + * Options to pass to the geolocation api. + * @type {Object} + */ + this._geolocationOptions = { + enableHighAccuracy: false, + timeout: 1000, + maximumAge: 300000, + ...config.geolocationOptions + }; + + /** + * Options for the geolocation timeout alert. + * @type {Object} + */ + this._geolocationTimeoutAlert = { + enabled: false, + message: TranslationFlagger.flag({ + phrase: 'We are unable to determine your location' + }), + ...config.geolocationTimeoutAlert + }; + + /** + * The unique HTML id name for the autocomplete container + * @type {string} + */ + this.autocompleteContainerIdName = `yxt-SearchBar-autocomplete--${this.name}`; + + /** + * The unique HTML id name for the search input label + * @type {string} + */ + this.inputLabelIdName = `yxt-SearchBar-inputLabel--${this.name}`; + + /** + * The unique HTML id name for the search input + * @type {string} + */ + this.inputIdName = `yxt-SearchBar-input--${this.name}`; + + this.customHooks = { + /** + * Callback invoked when the clear search button is clicked + */ + onClearSearch: (config.customHooks && config.customHooks.onClearSearch) || function () {}, + /** + * Callback invoked when a search is conducted + */ + onConductSearch: (config.customHooks && config.customHooks.onConductSearch) || function () {} + }; + + /** + * Options to pass to the autocomplete component + * @type {Object} + */ + this._autocompleteConfig = { + shouldHideOnEmptySearch: config.autocomplete && config.autocomplete.shouldHideOnEmptySearch, + onOpen: config.autocomplete && config.autocomplete.onOpen, + onClose: config.autocomplete && config.autocomplete.onClose + }; + + if (!this._isTwin) { + this.initQueryUpdateListener(); + } + } + + /** + * Updates the global search listener with the searchbar's config. + */ + initQueryUpdateListener () { + const queryUpdateListener = new QueryUpdateListener(this.core, { + searchCooldown: this._searchCooldown, + verticalKey: this._verticalKey, + allowEmptySearch: this._allowEmptySearch, + defaultInitialSearch: this._defaultInitialSearch + }); + this.core.setQueryUpdateListener(queryUpdateListener); + this.core.queryUpdateListener.registerMiddleware(query => this.promptForLocation(query)); + this.core.queryUpdateListener.registerMiddleware(query => this.customHooks.onConductSearch(query)); } static get type () { return 'SearchBar'; } - onCreate () { - if (this.query && this.query.length > 0) { + /** + * The template to render + * @returns {string} + * @override + */ + static defaultTemplateName () { + return 'search/search'; + } + + /** + * This method is called by answers-umd AFTER the onReady() is finished, and + * all components have been mounted. + */ + searchAfterAnswersOnReady () { + if (this.query != null && !this.redirectUrl) { this.core.setQuery(this.query); - this.search(this.query); } - - this.bindBrowserHistory(); } onMount () { + this.queryEl = DOM.query(this._container, this._inputEl); + if (this.autoFocus && !this.query && !this.autocompleteOnLoad) { + this.focusInputElement(); + } + + this.isUsingYextAnimatedIcon = !this._config.customIconUrl && !this.submitIcon; + if (this.isUsingYextAnimatedIcon) { + this.initAnimatedIcon(); + } + // Wire up our search handling and auto complete this.initSearch(this._formEl); this.initAutoComplete(this._inputEl); - if (this.autoFocus === true && this.query.length === 0) { - DOM.query(this._container, this._inputEl).focus(); + if (this.clearButton) { + this.initClearButton(); + } + + if (this.autoFocus && !this.query && this.autocompleteOnLoad) { + this.focusInputElement(); + } + } + + requestIconAnimationFrame (iconState) { + if (this.iconState === iconState) { + return; + } + this.iconState = iconState; + if (!this.isRequestingAnimationFrame) { + this.isRequestingAnimationFrame = true; + window.requestAnimationFrame(() => { + this.forwardIcon.classList.remove('yxt-SearchBar-AnimatedIcon--paused'); + this.reverseIcon.classList.remove('yxt-SearchBar-AnimatedIcon--paused'); + if (this.iconState === IconState.MAGNIFYING_GLASS) { + this.forwardIcon.classList.remove('yxt-SearchBar-AnimatedIcon--inactive'); + this.reverseIcon.classList.add('yxt-SearchBar-AnimatedIcon--inactive'); + } else if (this.iconState === IconState.YEXT) { + this.forwardIcon.classList.add('yxt-SearchBar-AnimatedIcon--inactive'); + this.reverseIcon.classList.remove('yxt-SearchBar-AnimatedIcon--inactive'); + } + this.isRequestingAnimationFrame = false; + }); } } + animateIconToMagnifyingGlass () { + if (this.iconIsFrozen) { + return; + } + this.requestIconAnimationFrame(IconState.MAGNIFYING_GLASS); + } + + animateIconToYext (e) { + let focusStillInSearchbar = false; + if (e && e.relatedTarget) { + focusStillInSearchbar = this._container.contains(e.relatedTarget); + } + if (this.iconIsFrozen || focusStillInSearchbar) { + return; + } + this.requestIconAnimationFrame(IconState.YEXT); + } + + initAnimatedIcon () { + this.iconState = (this.autoFocus && !this.query) ? IconState.MAGNIFYING_GLASS : IconState.YEXT; + this.forwardIcon = DOM.query(this._container, '.js-yxt-AnimatedForward'); + this.reverseIcon = DOM.query(this._container, '.js-yxt-AnimatedReverse'); + const clickableElementSelectors = ['.js-yext-submit', '.js-yxt-SearchBar-clear']; + for (const selector of clickableElementSelectors) { + const clickableEl = DOM.query(this._container, selector); + if (clickableEl) { + DOM.on(clickableEl, 'mousedown', () => { + this.iconIsFrozen = true; + }); + DOM.on(clickableEl, 'mouseup', () => { + this.iconIsFrozen = false; + }); + } + } + DOM.on(this.queryEl, 'focus', () => { + this.animateIconToMagnifyingGlass(); + }); + DOM.on(this._container, 'focusout', e => { + this.animateIconToYext(e); + }); + } + + remove () { + this._autocomplete.remove(); + super.remove(); + } + + initClearButton () { + const button = this._getClearButton(); + this._showClearButton = this._showClearButton || this.query; + button.classList.toggle('yxt-SearchBar--hidden', !this._showClearButton); + + DOM.on(button, 'click', () => { + this.customHooks.onClearSearch(); + this.query = ''; + this._showClearButton = false; + button.classList.add('yxt-SearchBar--hidden'); + this.queryEl.value = this.query; + + this.core.storage.delete(StorageKeys.SEARCH_OFFSET); + this.core.triggerSearch(QueryTriggers.SEARCH_BAR, this.query); + + // Focus the input element after clearing the query, regardless of whether + // or not the autoFocus option is enabled. + // NOTE(amullings): This depends heavily on the fact that the re-renders + // triggered by setState and core.setQuery happen synchronously; if this + // stops being the case at some point, we'll need an alternative solution + this.focusInputElement(); + }); + + DOM.on(this.queryEl, 'input', e => { + const input = e.target.value; + this.query = input; + this._updateClearButtonVisibility(input); + }); + } + /** - * A helper method to use for wiring up searching on form submission - * @param {string} formSelector CSS selector to bind our submit handling to + * Registers the different event handlers that can issue a search. Note that + * different handlers are used depending on whether or not a form is used as + * context. + * + * @param {string} formSelector CSS selector to bind our form submit handling to */ initSearch (formSelector) { this._formEl = formSelector; - let form = DOM.query(this._container, formSelector); - if (!form) { - throw new Error('Could not initialize SearchBar; Can not find {HTMLElement} `', this._formEl, '`.'); - } + this._container.classList.add('yxt-SearchBar-wrapper'); + + if (this._useForm) { + let form = DOM.query(this._container, formSelector); + if (!form) { + throw new Error( + 'Could not initialize SearchBar; Can not find {HTMLElement} `', + this._formEl, '`.'); + } - DOM.on(form, 'submit', (e) => { - e.preventDefault(); + DOM.on(form, 'submit', (e) => { + e.preventDefault(); + // TODO(oshi) we should not use the same css selector (this._inputEl) + // For both the autocomplete AND the search bar input + // This is incredibly confusing, and also makes the first DOM.query + // Rely on the order of the input el and autocomplete in the template + const inputEl = form.querySelector(this._inputEl); + this.onQuerySubmit(inputEl); + }); + } else { + const inputEl = DOM.query(this._container, this._inputEl); + if (!inputEl) { + throw new Error( + 'Could not initialize SearchBar; Can not find {HTMLElement} `', + this._inputEl, '`.'); + } + DOM.on(inputEl, 'keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + this.onQuerySubmit(inputEl); + } + }); + + const submitButton = DOM.query(this._container, '.js-yext-submit'); + DOM.on(submitButton, 'click', (e) => { + e.preventDefault(); + this.onQuerySubmit(inputEl); + }); + } + } - let query = form.querySelector(this._inputEl).value; - let params = this.getUrlParams(); - params.set('query', query); + /** + * The handler for a query submission. This method first sets the new query in + * persistent and storage, than performs a debounced search. + * + * @param {Node} inputEl The input element containing the query. + */ + onQuerySubmit (inputEl) { + const query = inputEl.value; + this.query = query; + const params = new SearchParams(this.core.storage.getCurrentStateUrlMerged()); + params.set('query', query); + + const context = this.core.storage.get(StorageKeys.API_CONTEXT); + if (context) { + params.set(StorageKeys.API_CONTEXT, context); + } - // If we have a redirectUrl, we want the form to be - // serialized and submitted. - if (typeof this.redirectUrl === 'string') { - window.location.href = this.redirectUrl + '?' + params.toString(); + // If we have a redirectUrl, we want the form to be + // serialized and submitted. + if (typeof this.redirectUrl === 'string') { + if (this._allowEmptySearch || query) { + const newUrl = this.redirectUrl + '?' + params.toString(); + window.open(newUrl, this.redirectUrlTarget) || (window.location.href = newUrl); return false; } + } - window.history.pushState({ - query: query - }, query, '?' + params.toString()); + inputEl.blur(); + DOM.query(this._container, '.js-yext-submit').blur(); + // TODO: move this into initClearButton + if (this.clearButton) { + const button = DOM.query(this._container, '.js-yxt-SearchBar-clear'); + if (this.query) { + this._showClearButton = true; + button.classList.remove('yxt-SearchBar--hidden'); + } else { + this._showClearButton = false; + button.classList.add('yxt-SearchBar--hidden'); + } + } + if (this.isUsingYextAnimatedIcon) { + this.animateIconToYext(); + } - this.core.setQuery(query); - this.search(query); - return false; - }); + this.core.storage.delete(StorageKeys.SEARCH_OFFSET); + this.core.triggerSearch(QueryTriggers.SEARCH_BAR, query); + return false; } /** @@ -190,81 +578,184 @@

Source: ui/components/search/searchcomponent.js

initAutoComplete (inputSelector) { this._inputEl = inputSelector; - this.componentManager.create('AutoComplete', { - parent: this, - name: `${this.name}.autocomplete`, + if (this._autocomplete) { + this._autocomplete.remove(); + } + + this._autocomplete = this.componentManager.create('AutoComplete', { + parentContainer: this._container, + name: this._autoCompleteName, container: '.yxt-SearchBar-autocomplete', - barKey: this._barKey, + autoFocus: this.autoFocus && !this.autocompleteOnLoad, verticalKey: this._verticalKey, promptHeader: this.promptHeader, originalQuery: this.query, inputEl: inputSelector, + listLabelIdName: this.inputLabelIdName, + ...this._autocompleteConfig, onSubmit: () => { - DOM.trigger('form', 'submit'); + if (this._useForm) { + DOM.trigger(DOM.query(this._container, this._formEl), 'submit'); + } else { + const inputEl = DOM.query(this._container, inputSelector); + this.onQuerySubmit(inputEl); + } + }, + onChange: () => { + DOM.trigger(DOM.query(this._container, inputSelector), 'input'); } }); + this._autocomplete.mount(); } - search (query) { - if (this._verticalKey) { - const allFilters = this.core.storage.getAll(StorageKeys.FILTER); - const totalFilter = allFilters.length > 1 - ? Filter.and(...allFilters) - : allFilters[0]; - return this.core.verticalSearch(query, this._verticalKey, JSON.stringify(totalFilter)); - } else { - // NOTE(billy) Temporary hack for DEMO - // Remove me after the demo - let nav = this.componentManager - .getActiveComponent('Navigation'); - - if (nav) { - let tabs = nav.getState('tabs'); - let urls = {}; - - if (tabs && Array.isArray(tabs)) { - for (let i = 0; i < tabs.length; i++) { - let params = this.getUrlParams(tabs[i].url.split('?')[1]); - params.set('query', query); - - let url = tabs[i].baseUrl; - if (params.toString().length > 0) { - url += '?' + params.toString(); - } - urls[tabs[i].configId] = url; + /** + * If _promptForLocation is enabled, we will compute the query's intent and, from there, + * determine if it's necessary to prompt the user for their location information. It will + * be unnecessary if the query does not have near me intent or we already have their location + * stored. + * @param {string} query The string to query against. + * @returns {Promise} A promise that will perform the query and update storage accordingly. + */ + promptForLocation (query) { + if (this._promptForLocation) { + return this.fetchQueryIntents(query) + .then(queryIntents => queryIntents.includes('NEAR_ME')) + .then(queryHasNearMeIntent => { + if (queryHasNearMeIntent && !this.core.storage.get(StorageKeys.GEOLOCATION)) { + return new Promise((resolve, reject) => + navigator.geolocation.getCurrentPosition( + position => { + this.core.storage.set(StorageKeys.GEOLOCATION, { + lat: position.coords.latitude, + lng: position.coords.longitude, + radius: position.coords.accuracy + }); + resolve(); + }, + () => { + resolve(); + const { enabled, message } = this._geolocationTimeoutAlert; + if (enabled) { + window.alert(message); + } + }, + this._geolocationOptions) + ); } - } - return this.core.search(query, urls); - } + }); + } else { + return Promise.resolve(); + } + } - return this.core.search(query); + /** + * A helper method that computes the intents of the provided query. If the query was entered + * manually into the search bar or selected via autocomplete, its intents will have been stored + * already in storage. Otherwise, a new API call will have to be issued to determine + * intent. + * @param {string} query The query whose intent is needed. + * @returns {Promise} A promise containing the intents of the query. + */ + fetchQueryIntents (query) { + const autocompleteData = + this.core.storage.get(`${StorageKeys.AUTOCOMPLETE}.${this._autoCompleteName}`); + if (!autocompleteData) { + const autocompleteRequest = this._verticalKey + ? this.core.autoCompleteVertical( + query, + this._autoCompleteName, + this._verticalKey) + : this.core.autoCompleteUniversal(query, this._autoCompleteName); + return autocompleteRequest.then(data => data.inputIntents); + } else { + // There are two alternatives to consider here. The user could have selected the query + // as an autocomplete option or manually input it themselves. If the former, use the intents + // of the corresponding autocomplete option. If the latter, use the inputIntents of the + // autocompleteData. + const results = autocompleteData.sections.flatMap(section => section.results); + const matchingResult = results.find(result => result.value === query); + const queryIntents = matchingResult ? matchingResult.intents : autocompleteData.inputIntents; + return Promise.resolve(queryIntents); } } + /** + * A helper method that constructs the meta information needed by the SEARCH_CLEAR_BUTTON + * analytics event. + */ + eventOptions () { + const queryId = this.core.storage.get(StorageKeys.QUERY_ID); + const options = Object.assign( + {}, + queryId && { queryId }, + this._verticalKey && { verticalKey: this._verticalKey } + ); + return JSON.stringify(options); + } + setState (data) { + const forwardIconOpts = { + iconName: 'yext_animated_forward', + classNames: 'Icon--lg', + complexContentsParams: { + iconPrefix: this.name + } + }; + const reverseIconOpts = { + iconName: 'yext_animated_reverse', + classNames: 'Icon--lg', + complexContentsParams: { + iconPrefix: this.name + } + }; return super.setState(Object.assign({ title: this.title, - searchText: this.searchText, - query: this.query + inputIdName: this.inputIdName, + labelText: this.labelText, + inputLabelIdName: this.inputLabelIdName, + submitIcon: this.submitIcon, + submitText: this.submitText, + clearText: this.clearText, + showClearButton: this._showClearButton, + query: this.query || '', + eventOptions: this.eventOptions(), + iconId: this.name, + forwardIconOpts: forwardIconOpts, + reverseIconOpts: reverseIconOpts, + autoFocus: this.autoFocus && !this.query, + useForm: this._useForm, + autocompleteContainerIdName: this.autocompleteContainerIdName }, data)); } - getUrlParams (url) { - url = url || window.location.search.substring(1); - return new URLSearchParams(url); + focusInputElement () { + DOM.query(this._container, this._inputEl).focus(); } - bindBrowserHistory () { - DOM.on(window, 'popstate', () => { - this.query = this.getUrlParams().get('query'); - this.setState({ - query: this.query - }); - - this.core.setQuery(this.query); + /** + * Returns the clear button element, if exists + * + * @returns {Element} + */ + _getClearButton () { + return DOM.query(this._container, '.js-yxt-SearchBar-clear'); + } - this.search(this.query); - }); + /** + * Updates the Search inputs clear button based on the current input value + * + * @param {string} input + */ + _updateClearButtonVisibility (input) { + const clearButton = this._getClearButton(); + + if (!this._showClearButton && input.length > 0) { + this._showClearButton = true; + clearButton.classList.remove('yxt-SearchBar--hidden'); + } else if (this._showClearButton && input.length === 0) { + this._showClearButton = false; + clearButton.classList.add('yxt-SearchBar--hidden'); + } } }
@@ -277,13 +768,13 @@

Source: ui/components/search/searchcomponent.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/ui_components_search_spellcheckcomponent.js.html b/docs/ui_components_search_spellcheckcomponent.js.html new file mode 100644 index 000000000..d031f091c --- /dev/null +++ b/docs/ui_components_search_spellcheckcomponent.js.html @@ -0,0 +1,114 @@ + + + + + JSDoc: Source: ui/components/search/spellcheckcomponent.js + + + + + + + + + + +
+ +

Source: ui/components/search/spellcheckcomponent.js

+ + + + + + +
+
+
/** @module SpellCheckComponent */
+
+import Component from '../component';
+import SearchParams from '../../dom/searchparams';
+import StorageKeys from '../../../core/storage/storagekeys';
+import TranslationFlagger from '../../i18n/translationflagger';
+
+const DEFAULT_CONFIG = {
+  suggestionHelpText: TranslationFlagger.flag({
+    phrase: 'Did you mean:',
+    context: 'Help text, labels a suggested phrase'
+  })
+};
+
+/**
+ * SpellCheckComponent will support displaying suggestion, autocorrect and combined(maybe in the future)
+ * provided from spelling correction.
+ *
+ * @extends Component
+ */
+export default class SpellCheckComponent extends Component {
+  constructor (config = {}, systemConfig = {}) {
+    super({ ...DEFAULT_CONFIG, ...config }, systemConfig);
+
+    this.moduleId = StorageKeys.SPELL_CHECK;
+  }
+
+  static get type () {
+    return 'SpellCheck';
+  }
+
+  static defaultTemplateName () {
+    return 'search/spellcheck';
+  }
+
+  setState (data, val) {
+    return super.setState(Object.assign({}, data, {
+      shouldShow: data.correctedQuery !== undefined,
+      correctedQueryUrl: this._buildRedirectQueryUrl(data.correctedQuery, data.type),
+      helpText: this._getHelpText(data.type)
+    }, val));
+  }
+
+  _buildRedirectQueryUrl (query, type) {
+    if (query === undefined) {
+      return '';
+    }
+    let params = new SearchParams(this.core.storage.getCurrentStateUrlMerged());
+    params.set(StorageKeys.QUERY, query.value);
+    params.set(StorageKeys.SKIP_SPELL_CHECK, true);
+    params.set(StorageKeys.QUERY_TRIGGER, type.toLowerCase());
+    return '?' + params.toString();
+  }
+
+  _getHelpText (type) {
+    switch (type) {
+      case 'SUGGEST':
+        return this._config.suggestionHelpText;
+      default:
+        return '';
+    }
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/ui_components_state.js.html b/docs/ui_components_state.js.html index 8a1f9154f..572490ed2 100644 --- a/docs/ui_components_state.js.html +++ b/docs/ui_components_state.js.html @@ -116,13 +116,13 @@

Source: ui/components/state.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/ui_dom_dom.js.html b/docs/ui_dom_dom.js.html index f12060ad6..3cf1cee60 100644 --- a/docs/ui_dom_dom.js.html +++ b/docs/ui_dom_dom.js.html @@ -28,10 +28,9 @@

Source: ui/dom/dom.js

/** @module DOM */
 
-/* global HTMLElement, HTMLDocument, Window, Event */
+/* global HTMLElement, HTMLDocument, Window, Element */
 
 let document = window.document;
-let parser = new DOMParser();
 
 /**
  * Static interface for interacting with the DOM API.
@@ -40,7 +39,6 @@ 

Source: ui/dom/dom.js

export default class DOM { static setup (d, p) { document = d; - parser = p; } /** @@ -49,7 +47,19 @@

Source: ui/dom/dom.js

* @return {HTMLElement} */ static create (html) { - return parser.parseFromString(html, 'text/html').body; + if ('createRange' in document) { + // prefer this implementation as it has wider browser support + // and it's better performing. + // see https://davidwalsh.name/convert-html-stings-dom-nodes + const container = document.createElement('div'); + const frag = document.createRange().createContextualFragment(html); + container.appendChild(frag); + return container; + } + + // fallback to this because of a bug in jsdom that causes tests to fail + // see: https://github.com/jsdom/jsdom/issues/399 + return new DOMParser().parseFromString(html, 'text/html').body; } /** @@ -79,7 +89,7 @@

Source: ui/dom/dom.js

* @param {HTMLElement} parent Optional context to use for a search. Defaults to document if not provided. * @param {string} selector the CSS selector to query for * - * @returns {HTMLElement} the FIRST node it finds, if any + * @returns {Array} the FIRST node it finds, if any */ static queryAll (parent, selector) { // Facade, shifting the selector to the parent argument if only one @@ -89,11 +99,16 @@

Source: ui/dom/dom.js

parent = document; } + // handle the case where client code is using a pointer to a dom node and it's null, e.g. this._container + if (parent == null) { + parent = document; + } + if (selector instanceof HTMLElement || selector instanceof HTMLDocument || selector instanceof Window) { - return selector; + return [selector]; } - return parent.querySelectorAll(selector); + return Array.from(parent.querySelectorAll(selector)); } static onReady (cb) { @@ -145,6 +160,10 @@

Source: ui/dom/dom.js

} static addClass (node, className) { + if (!node) { + return; + } + let classes = className.split(','); let len = classes.length; @@ -153,6 +172,19 @@

Source: ui/dom/dom.js

} } + /** + * Removes classes from a specified element. + * @param {HTMLElement} node The html element to be acted upon + * @param {string} className A css class to be removed + */ + static removeClass (node, className) { + if (!node) { + return; + } + + node.classList.remove(className); + } + static empty (parent) { parent.innerHTML = ''; } @@ -169,19 +201,38 @@

Source: ui/dom/dom.js

DOM.query(selector).setAttribute(attr, val); } - static trigger (selector, event, settings) { - let e = new Event(event, Object.assign({ - 'bubbles': true, - 'cancelable': true - }, settings || {})); + static attributes (selector, attrs) { + Object.entries(attrs) + .forEach(([attr, val]) => this.attr(selector, attr, val)); + } + static trigger (selector, event, settings) { + let e = DOM._customEvent(event, settings); DOM.query(selector).dispatchEvent(e); } + // TODO (agrow) investigate removing this + // Event constructor polyfill + static _customEvent (event, settings) { + const _settings = { + bubbles: true, + cancelable: true, + detail: null, + ...settings + }; + const evt = document.createEvent('CustomEvent'); + evt.initCustomEvent(event, _settings.bubbles, _settings.cancelable, _settings.detail); + return evt; + } + static on (selector, evt, handler) { DOM.query(selector).addEventListener(evt, handler); } + static once (selector, evt, handler) { + DOM.query(selector).addEventListener(evt, handler, { once: true }); + } + static off (selector, evt, handler) { DOM.query(selector).removeEventListener(evt, handler); } @@ -191,7 +242,7 @@

Source: ui/dom/dom.js

el.addEventListener(evt, function (event) { let target = event.target; while (!target.isEqualNode(el)) { - if (target.matches(selector)) { + if (DOM.matches(target, selector)) { handler(event, target); break; } @@ -199,6 +250,20 @@

Source: ui/dom/dom.js

} }); } + + // TODO (agrow) investigate removing this + // Element.matches polyfill + static matches (element, potentialMatch) { + if (Element.prototype.matches) { + return element.matches(potentialMatch); + } + if (Element.prototype.msMatchesSelector) { + return element.msMatchesSelector(potentialMatch); + } + if (Element.prototype.webkitMatchesSelector) { + return element.webkitMatchesSelector(potentialMatch); + } + } }
@@ -210,13 +275,13 @@

Source: ui/dom/dom.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/ui_dom_searchparams.js.html b/docs/ui_dom_searchparams.js.html new file mode 100644 index 000000000..4380c8601 --- /dev/null +++ b/docs/ui_dom_searchparams.js.html @@ -0,0 +1,197 @@ + + + + + JSDoc: Source: ui/dom/searchparams.js + + + + + + + + + + +
+ +

Source: ui/dom/searchparams.js

+ + + + + + +
+
+
/** @module SearchParams */
+
+/* global window */
+
+/**
+ * SearchParams is a class to get the search params in a URL.
+ * It is a replacement for URL.searchParams and URLSearchParams for browsers like IE11
+ */
+export default class SearchParams {
+  constructor (url) {
+    /**
+     * Mapping of all query parameters in the given url, query param -> value
+     * Only used if URLSearchParams does not exist in the window
+     * @type {Object}
+     * @private
+     */
+    this._params = {};
+
+    if (window && window.URLSearchParams) {
+      return new URLSearchParams(url);
+    } else {
+      this._params = this.parse(url);
+    }
+  }
+
+  /**
+   * parse creates a mapping of all query params in a given url
+   * The query param values are decoded before being put in the map
+   * Three types of input are supported
+   *   (1) full URL e.g. http://www.yext.com/?q=hello
+   *   (2) params with ? e.g. ?q=hello
+   *   (1) params without ? e.g. q=hello
+   * @param {string} url The url
+   * @returns {Object} mapping from query param -> value where value is '' if no value is provided
+   */
+  parse (url) {
+    let params = {};
+    let search = url;
+
+    if (!search) {
+      return params;
+    }
+
+    // Normalize all url inputs to string of query params separated by &
+    if (url.indexOf('?') > -1) {
+      search = url.slice(url.indexOf('?') + 1);
+    }
+
+    const encodedParams = search.split('&');
+    for (let i = 0; i < encodedParams.length; i++) {
+      const keyVal = encodedParams[i].split('=');
+      if (keyVal.length > 1) {
+        params[keyVal[0]] = SearchParams.decode(keyVal[1]);
+      } else {
+        params[keyVal[0]] = '';
+      }
+    }
+
+    return params;
+  }
+
+  /**
+   * get returns the value of the given query param
+   * @param {string} query the query param key to get the value of
+   * @return {string} param value, null otherwise
+   */
+  get (query) {
+    if (typeof this._params[String(query)] === 'undefined') {
+      return null;
+    }
+    return this._params[query];
+  }
+
+  /**
+   * set changes the value of a given query param
+   * @param {string} name the query param key
+   * @param {string} value the value of the query param update with
+   */
+  set (name, value) {
+    this._params[String(name)] = String(value);
+  }
+
+  /**
+   * has checks to see if the given query param key exists in the params object
+   * @param {string} query the query param to check
+   * @return {boolean} true if the query param is in the params object, false o/w
+   */
+  has (query) {
+    return query in this._params;
+  }
+
+  /**
+   * delete removes the given query param and its associated value from the params object
+   * @param {string} name the query param key
+   */
+  delete (name) {
+    delete this._params[String(name)];
+  }
+
+  /**
+   * toString returns a url with all the query params in the params object (without a ?)
+   * @return {string}
+   */
+  toString () {
+    let string = [];
+    for (let key in this._params) {
+      string.push(`${key}=${SearchParams.encode(this._params[key])}`);
+    }
+    return string.join('&');
+  }
+
+  entries () {
+    let entries = [];
+    for (let key in this._params) {
+      entries.push([key, this._params[key]]);
+    }
+    return entries;
+  }
+
+  /**
+   * decode returns the decoded representation of the given string
+   * @param {string} string the string to decode
+   * @return {string}
+   */
+  static decode (string) {
+    return decodeURIComponent(string.replace(/[ +]/g, '%20'));
+  }
+
+  /**
+   * decode returns the encoded representation of the given string (e.g. + -> %2B)
+   * @param {string} string the string to encode
+   * @return {string}
+   */
+  static encode (string) {
+    let replace = {
+      '!': '%21',
+      "'": '%27',
+      '(': '%28',
+      ')': '%29',
+      '%20': '+'
+    };
+    return encodeURIComponent(string).replace(/[!'()]|%20/g, function (match) {
+      return replace[match];
+    });
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/ui_i18n_translationflagger.js.html b/docs/ui_i18n_translationflagger.js.html new file mode 100644 index 000000000..e83c8c313 --- /dev/null +++ b/docs/ui_i18n_translationflagger.js.html @@ -0,0 +1,85 @@ + + + + + JSDoc: Source: ui/i18n/translationflagger.js + + + + + + + + + + +
+ +

Source: ui/i18n/translationflagger.js

+ + + + + + +
+
+
/**
+ * TranslationFlagger is a class used to flag Translation calls. The usages of this class
+ * are handled and removed during SDK bundling.
+ */
+export default class TranslationFlagger {
+  /**
+   * Any calls of this method will be removed during a preprocessing step during SDK
+   * bundling.
+   *
+   * To support cases where someone may want to bundle without using our
+   * bundling tasks, this function attempts to return the same-language interpolated
+   * and pluralized value based on the information given.
+   *
+   * @param {string} phrase
+   * @param {string} pluralForm
+   * @param {string | number} count
+   * @param {string} context
+   * @param {Object} interpolationValues
+   * @returns {string}
+   */
+  static flag ({ phrase, pluralForm, count, context, interpolationValues }) {
+    const isPlural = count && count > 1 && pluralForm;
+    const declensionOfPhrase = isPlural ? pluralForm : phrase;
+    if (!interpolationValues) {
+      return declensionOfPhrase;
+    }
+
+    let interpolatedPhrase = declensionOfPhrase;
+    for (const [key, value] of Object.entries(interpolationValues)) {
+      interpolatedPhrase = interpolatedPhrase.replace(`[[${key}]]`, value);
+    }
+    return interpolatedPhrase;
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/ui_icons_icon.js.html b/docs/ui_icons_icon.js.html new file mode 100644 index 000000000..c937db928 --- /dev/null +++ b/docs/ui_icons_icon.js.html @@ -0,0 +1,110 @@ + + + + + JSDoc: Source: ui/icons/icon.js + + + + + + + + + + +
+ +

Source: ui/icons/icon.js

+ + + + + + +
+
+
export default class SVGIcon {
+  /**
+   * @param config
+   * @param config.name
+   * @param config.path
+   * @param config.complexContents
+   * @param config.viewBox
+   * @constructor
+   */
+  constructor (config) {
+    /**
+     * the name of the icon
+     */
+    this.name = config.name;
+    /**
+     * an svg path definition
+     */
+    this.path = config.path;
+    /**
+     * if not using a path, a the markup for a complex SVG
+     */
+    this.complexContents = config.complexContents;
+    /**
+     * the view box definition, defaults to 24x24
+     * @type {string}
+     */
+    this.viewBox = config.viewBox || '0 0 24 24';
+    /**
+     * actual contents used
+     */
+    this.contents = this.pathDefinition();
+  }
+
+  pathDefinition () {
+    if (this.complexContents) {
+      return this.complexContents;
+    }
+
+    return `<path d="${this.path}"></path>`;
+  }
+
+  parseContents (complexContentsParams) {
+    let contents = this.contents;
+    if (typeof contents === 'function') {
+      contents = contents(complexContentsParams);
+    }
+    return `<svg viewBox="${this.viewBox}" xmlns="http://www.w3.org/2000/svg">${contents}</svg>`;
+  }
+
+  /**
+   * returns the svg markup
+   */
+  markup () {
+    if (typeof this.contents === 'function') {
+      return complexContentsParams => this.parseContents(complexContentsParams);
+    }
+    return this.parseContents();
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/ui_index.js.html b/docs/ui_index.js.html index c266b96fd..077c09dbe 100644 --- a/docs/ui_index.js.html +++ b/docs/ui_index.js.html @@ -28,11 +28,11 @@

Source: ui/index.js

/** @module */
 
-export { COMPONENT_MANAGER } from './components/const';
 export { default as DOM } from './dom/dom';
+export { default as SearchParams } from './dom/searchparams';
 
 export { Renderers } from './rendering/const';
-export { default as TemplateLoader } from './rendering/templateloader';
+export { default as DefaultTemplatesLoader } from './rendering/defaulttemplatesloader';
 
@@ -43,13 +43,13 @@

Source: ui/index.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/ui_rendering_const.js.html b/docs/ui_rendering_const.js.html index 27be423da..ea6f450f0 100644 --- a/docs/ui_rendering_const.js.html +++ b/docs/ui_rendering_const.js.html @@ -47,13 +47,13 @@

Source: ui/rendering/const.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/ui_rendering_defaulttemplatesloader.js.html b/docs/ui_rendering_defaulttemplatesloader.js.html new file mode 100644 index 000000000..f37efb379 --- /dev/null +++ b/docs/ui_rendering_defaulttemplatesloader.js.html @@ -0,0 +1,128 @@ + + + + + JSDoc: Source: ui/rendering/defaulttemplatesloader.js + + + + + + + + + + +
+ +

Source: ui/rendering/defaulttemplatesloader.js

+ + + + + + +
+
+
/** @module DefaultTemplatesLoader */
+
+import DOM from '../dom/dom';
+import { COMPILED_TEMPLATES_URL } from '../../core/constants';
+
+/**
+ * DefaultTemplatesLoader exposes an interface for loading the default set of compiled templates
+ * asynchronously from the server. Note that this class cannot be repurposed to fetch custom
+ * templates hosted by a client.
+ */
+export default class DefaultTemplatesLoader {
+  constructor (onLoaded) {
+    if (!DefaultTemplatesLoader.setInstance(this)) {
+      return DefaultTemplatesLoader.getInstance();
+    }
+    this._templates = {};
+    this._onLoaded = onLoaded || function () {};
+  }
+
+  static setInstance (instance) {
+    if (!this.instance) {
+      this.instance = instance;
+      return true;
+    }
+    return false;
+  }
+
+  static getInstance () {
+    return this.instance;
+  }
+
+  fetchTemplates () {
+    // If template have already been loaded, do nothing
+    let node = DOM.query('#yext-answers-templates');
+    if (node) {
+      return Promise.resolve();
+    }
+
+    // Inject a script to fetch the compiled templates,
+    // wrapping it a Promise for cleanliness
+    return new Promise((resolve, reject) => {
+      let script = DOM.createEl('script', {
+        id: 'yext-answers-templates',
+        onload: resolve,
+        onerror: reject,
+        async: true,
+        src: COMPILED_TEMPLATES_URL
+      });
+      DOM.append('body', script);
+    });
+  }
+
+  /**
+   * register the templates internally so that they can be later consumed
+   * (e.g. by components and renderers) with convienience.
+   *
+   * This is called from inside handlebarswrapper.txt.
+   */
+  register (templates) {
+    this._templates = templates;
+
+    // Notify our consumers that the templates are here :)
+    this._onLoaded(this._templates);
+    return this;
+  }
+
+  get (templateName) {
+    return this._templates[templateName];
+  }
+
+  /**
+   * @return The internal template collection
+   */
+  getTemplates () {
+    return this._templates;
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/ui_rendering_handlebarsrenderer.js.html b/docs/ui_rendering_handlebarsrenderer.js.html index f0b4a8587..dc02bda22 100644 --- a/docs/ui_rendering_handlebarsrenderer.js.html +++ b/docs/ui_rendering_handlebarsrenderer.js.html @@ -29,6 +29,9 @@

Source: ui/rendering/handlebarsrenderer.js

/** @module HandlebarsRenderer */
 
 import Renderer from './renderer';
+import Icons from '../icons';
+import HighlightedValue from '../../core/models/highlightedvalue';
+import TranslationProcessor from '../../core/i18n/translationprocessor';
 
 /**
  * HandlebarsRenderer is a wrapper around the nativate handlebars renderer.
@@ -53,12 +56,15 @@ 

Source: ui/rendering/handlebarsrenderer.js

this._templates = templates || {}; } - init (templates) { + init (templates, locale) { // Assign the handlebars compiler and templates based on // information provided from external dep (in default case, it comes from external server request) this._handlebars = templates._hb; this._templates = templates; + // Store the locale that ANSWERS was initialized with + this._initLocale = locale; + // TODO(billy) Once we re-write templates using the new helpers library // we probably don't need these custom helpers anymore this._registerCustomHelpers(); @@ -72,10 +78,27 @@

Source: ui/rendering/handlebarsrenderer.js

this._handlebars.registerHelper(name, cb); } + /** + * SafeString is a public interface for external dependencies to + * mark a string as 'safe'. Handlebars will not escape a SafeString + */ + SafeString (string) { + return new this._handlebars.SafeString(string); + } + + /** + * EscapeExpression is a public interface for external dependencies to + * escape a string + */ + escapeExpression (string) { + return this._handlebars.escapeExpression(string); + } + /** * compile a handlebars template so that it can be rendered, * using the {Handlebars} compiler * @param {string} template The template string to compile + * @returns {Function} */ compile (template) { if (typeof template !== 'string') { @@ -84,6 +107,15 @@

Source: ui/rendering/handlebarsrenderer.js

return this._handlebars.compile(template); } + /** + * compile a template and then add it to the current template bundle + * @param {string} templateName The unique name for the template + * @param {string} template The handlebars template string + */ + registerTemplate (templateName, template) { + this._templates[templateName] = this.compile(template); + } + /** * render will render a template with data * @param {Object} config Provide either a templateName or a pre-compiled template @@ -97,10 +129,15 @@

Source: ui/rendering/handlebarsrenderer.js

return config.template(data); } + if (!(config.templateName in this._templates)) { + throw new Error('Can\'t find template: ' + config.templateName); + } + try { return this._templates[config.templateName](data); } catch (e) { - throw new Error('Can not find/render template: ' + config.templateName, e); + console.error('Error when trying to render the template: ' + config.templateName); + throw e; } } @@ -113,6 +150,47 @@

Source: ui/rendering/handlebarsrenderer.js

return (arg1 !== arg2) ? options.fn(this) : options.inverse(this); }); + this.registerHelper({ + eq: function (v1, v2) { + return v1 === v2; + }, + ne: function (v1, v2) { + return v1 !== v2; + }, + lt: function (v1, v2) { + return v1 < v2; + }, + gt: function (v1, v2) { + return v1 > v2; + }, + lte: function (v1, v2) { + return v1 <= v2; + }, + gte: function (v1, v2) { + return v1 >= v2; + }, + and: function () { + return Array.prototype.slice.call(arguments).every(Boolean); + }, + or: function () { + return Array.prototype.slice.call(arguments, 0, -1).some(Boolean); + } + }); + + this.registerHelper({ + add: (a1, a2) => a1 + a2, + sub: (a1, a2) => a1 - a2, + mul: (a1, a2) => a1 * a2, + div: (a1, a2) => a1 / a2, + mod: (a1, a2) => a1 % a2 + }); + + this.registerHelper('every', function (...args) { + const values = args.slice(0, args.length - 1); + const options = args[args.length - 1]; + return (values.every(v => v)) ? options.fn(this) : options.inverse(this); + }); + this.registerHelper('formatPhoneNumber', function (phoneNumberString) { var cleaned = ('' + phoneNumberString).replace(/\D/g, ''); var match = cleaned.match(/^(1|)?(\d{3})(\d{3})(\d{4})$/); @@ -144,6 +222,65 @@

Source: ui/rendering/handlebarsrenderer.js

? '' : JSON.stringify(name); }); + + this.registerHelper('plural', function (number, singularText, pluralText) { + return number === 1 + ? singularText + : pluralText; + }); + + let self = this; + + this.registerHelper('processTranslation', function (options) { + const pluralizationInfo = {}; + const interpolationParams = {}; + let { phrase, count, locale } = options.hash; + + Object.entries(options.hash).forEach(([key, value]) => { + if (key.startsWith('pluralForm')) { + const pluralFormIndex = parseInt(key.substring(10)); + pluralizationInfo[pluralFormIndex] = value; + } else { + interpolationParams[key] = value; + } + }); + + const isUsingPluralization = (typeof phrase !== 'string'); + + locale = locale || self._initLocale; + const language = locale.substring(0, 2); + + return isUsingPluralization + ? TranslationProcessor.process(pluralizationInfo, interpolationParams, count, language, self.escapeExpression.bind(self)) + : TranslationProcessor.process(phrase, interpolationParams, null, null, self.escapeExpression.bind(self)); + }); + + self.registerHelper('icon', function (name, complexContentsParams, options) { + let icon = Icons.default; + if (!Icons[name]) { + return self.SafeString(icon); + } + if (typeof Icons[name] === 'function') { + icon = Icons[name](complexContentsParams); + } else { + icon = Icons[name]; + } + return self.SafeString(icon); + }); + + self.registerHelper('highlightValue', function (value, getInverted) { + const input = value.value || value.shortValue; + + const highlightedVal = new HighlightedValue({ + value: input, + matchedSubstrings: value.matchedSubstrings + }); + const escapeFunction = (val) => self.escapeExpression(val); + + return getInverted + ? self.SafeString(highlightedVal.getInvertedWithTransformFunction(escapeFunction)) + : self.SafeString(highlightedVal.getWithTransformFunction(escapeFunction)); + }); } }
@@ -156,13 +293,13 @@

Source: ui/rendering/handlebarsrenderer.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/ui_rendering_renderer.js.html b/docs/ui_rendering_renderer.js.html index 85528af70..60cd34165 100644 --- a/docs/ui_rendering_renderer.js.html +++ b/docs/ui_rendering_renderer.js.html @@ -46,6 +46,10 @@

Source: ui/rendering/renderer.js

} + registerTemplate (templateName, template) { + + } + compile (template) { } @@ -60,13 +64,13 @@

Source: ui/rendering/renderer.js


- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/ui_rendering_templateloader.js.html b/docs/ui_rendering_templateloader.js.html deleted file mode 100644 index 84560124e..000000000 --- a/docs/ui_rendering_templateloader.js.html +++ /dev/null @@ -1,152 +0,0 @@ - - - - - JSDoc: Source: ui/rendering/templateloader.js - - - - - - - - - - -
- -

Source: ui/rendering/templateloader.js

- - - - - - -
-
-
/** @module TemplateLoader */
-
-import DOM from '../dom/dom';
-import { COMPILED_TEMPLATES_URL } from '../../core/constants';
-
-/**
- * TemplateLoader exposes an interface for loading templates asynchronously
- * from the server and registers them with the proper renderer.
- * It also allows you to assign them synchronously.
- */
-export default class TemplateLoader {
-  constructor (config) {
-    if (!TemplateLoader.setInstance(this)) {
-      return TemplateLoader.getInstance();
-    }
-
-    /**
-     * The template url to fetch compiled templates from
-     * @type {string}
-     * @private
-     */
-    this._templateUrl = config.templateUrl || COMPILED_TEMPLATES_URL;
-
-    this._templates = {};
-    this._onLoaded = function () {};
-    this._init();
-  }
-
-  static setInstance (instance) {
-    if (!this.instance) {
-      this.instance = instance;
-      return true;
-    }
-    return false;
-  }
-
-  static getInstance () {
-    return this.instance;
-  }
-
-  _init () {
-    this.fetchTemplates();
-  }
-
-  fetchTemplates () {
-    // If we already have templates loaded, do nothing
-    let node = DOM.query('#yext-answers-templates');
-    if (node) {
-      return;
-    }
-
-    // Inject a script to fetch the compiled templates,
-    // wrapping it a Promise for cleanliness
-    new Promise((resolve, reject) => {
-      let script = DOM.createEl('script', {
-        id: 'yext-answers-templates',
-        onload: resolve,
-        onerror: reject,
-        async: true,
-        src: this._templateUrl
-      });
-
-      DOM.append('body', script);
-    })
-      .then((response) => {
-      // TODO(billy) Implmenet error handling here (e.g. request could fail)
-        console.log('Templates loaded successfully!');
-      });
-    return this;
-  }
-
-  /**
-   * register the templates internally so that they can be later consumed
-   * (e.g. by components and renderers) with convienience.
-   *
-   * `fetchTemplates` will automatically call this, providing the compiled templates from the server.
-   */
-  register (templates) {
-    this._templates = templates;
-
-    // Notify our consumers that the templates are here :)
-    this._onLoaded(this._templates);
-    return this;
-  }
-
-  onLoaded (cb) {
-    this._onLoaded = cb;
-    return this;
-  }
-
-  get (templateName) {
-    return this._templates[templateName];
-  }
-
-  /**
-   * @return The internal template collection
-   */
-  getTemplates () {
-    return this._templates;
-  }
-}
-
-
-
- - - - -
- - - -
- -
- Documentation generated by JSDoc 3.6.2 on Fri Jul 19 2019 10:43:24 GMT-0400 (Eastern Daylight Time) -
- - - - - diff --git a/docs/ui_tools_filterutils.js.html b/docs/ui_tools_filterutils.js.html new file mode 100644 index 000000000..b763f8e94 --- /dev/null +++ b/docs/ui_tools_filterutils.js.html @@ -0,0 +1,114 @@ + + + + + JSDoc: Source: ui/tools/filterutils.js + + + + + + + + + + +
+ +

Source: ui/tools/filterutils.js

+ + + + + + +
+
+
import FilterCombinators from '../../core/filters/filtercombinators';
+import isEqual from 'lodash.isequal';
+import Filter from '../../core/models/filter';
+
+/**
+ * Checks whether a filter is equal to or included somewhere within the persistedFilter.
+ * Assumes the given filter is a simple filter, i.e. does not have any child filters.
+ * The persistedFilter can be either combined or simple.
+ *
+ * @param {Filter} filter
+ * @param {Filter} persistedFilter
+ * @returns {boolean}
+ */
+export function filterIsPersisted (filter, persistedFilter) {
+  const childFilters =
+    persistedFilter[FilterCombinators.AND] || persistedFilter[FilterCombinators.OR];
+  if (childFilters) {
+    return !!childFilters.find(childFilter => filterIsPersisted(filter, Filter.from(childFilter)));
+  }
+  return isEqual(filter, persistedFilter);
+}
+
+/**
+ * Given a filter, return an array of all it's descendants, including itself, that
+ * filter on the given fieldId.
+ *
+ * @param {Filter} persistedFilter
+ * @param {string} fieldId
+ *
+ * @returns {Array<Filter>}
+ */
+export function findSimpleFiltersWithFieldId (persistedFilter, fieldId) {
+  const childFilters =
+    persistedFilter[FilterCombinators.AND] || persistedFilter[FilterCombinators.OR];
+  if (childFilters) {
+    return childFilters.flatMap(
+      childFilter => findSimpleFiltersWithFieldId(Filter.from(childFilter), fieldId));
+  }
+  if (Filter.from(persistedFilter).getFilterKey() === fieldId) {
+    return [ persistedFilter ];
+  }
+  return [];
+}
+
+/**
+ * Finds a persisted range filter for the given fieldId, and returns its contents.
+ *
+ * @param {Filter} persistedFilter
+ * @param {string} fieldId
+ * @returns {{minVal: number, maxVal: number}}
+ */
+export function getPersistedRangeFilterContents (persistedFilter, fieldId) {
+  if (!persistedFilter || !persistedFilter.getFilterKey()) {
+    return {};
+  }
+  const rangeFiltersForFieldId =
+    findSimpleFiltersWithFieldId(persistedFilter, fieldId)
+      .filter(f => f.isRangeFilter());
+  if (rangeFiltersForFieldId.length < 1) {
+    return {};
+  }
+  return rangeFiltersForFieldId[0][fieldId];
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/ui_tools_searchparamsparser.js.html b/docs/ui_tools_searchparamsparser.js.html new file mode 100644 index 000000000..b16b63c8f --- /dev/null +++ b/docs/ui_tools_searchparamsparser.js.html @@ -0,0 +1,75 @@ + + + + + JSDoc: Source: ui/tools/searchparamsparser.js + + + + + + + + + + +
+ +

Source: ui/tools/searchparamsparser.js

+ + + + + + +
+
+
/** @module SearchParamsParser */
+
+export default function buildSearchParameters (searchParameterConfigs) {
+  let searchParameters = {
+    sectioned: false,
+    fields: []
+  };
+  if (searchParameterConfigs === undefined) {
+    return searchParameters;
+  }
+  if (searchParameterConfigs.sectioned) {
+    searchParameters.sectioned = searchParameterConfigs.sectioned;
+  }
+  searchParameters.fields = buildFields(searchParameterConfigs.fields);
+  return searchParameters;
+}
+
+function buildFields (fieldConfigs) {
+  if (fieldConfigs === undefined) {
+    return [];
+  }
+
+  return fieldConfigs.map(fc => ({ fetchEntities: false, ...fc }));
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/docs/ui_tools_taborder.js.html b/docs/ui_tools_taborder.js.html new file mode 100644 index 000000000..6e5aeb2d7 --- /dev/null +++ b/docs/ui_tools_taborder.js.html @@ -0,0 +1,116 @@ + + + + + JSDoc: Source: ui/tools/taborder.js + + + + + + + + + + +
+ +

Source: ui/tools/taborder.js

+ + + + + + +
+
+
import SearchParams from '../dom/searchparams';
+
+/**
+ * @param {string} urlParams a query param string like "query=hi&otherParam=hello"
+ * @returns {SearchParams}
+ */
+export function getUrlParams (urlParams) {
+  return new SearchParams(urlParams);
+}
+
+export function getDefaultTabOrder (tabsConfig, urlParams) {
+  let tabOrder = [];
+  // Use the ordering from the URL as the primary configuration
+  // And then merge it with the local configuration, if provided.
+  if (urlParams && urlParams.has('tabOrder')) {
+    tabOrder = urlParams.get('tabOrder').split(',');
+  }
+  for (const tab of tabsConfig) {
+    const verticalKeyOrUrl = tab.verticalKey || tab.url;
+    // Avoid duplicates if config was provided from URL
+    if (tabOrder.includes(verticalKeyOrUrl)) {
+      continue;
+    }
+
+    // isFirst should always be the first element in the list
+    if (tab.isFirst) {
+      tabOrder.unshift(verticalKeyOrUrl);
+    } else {
+      tabOrder.push(verticalKeyOrUrl);
+    }
+  }
+  return tabOrder;
+}
+
+/**
+ * mergeTabOrder merges two arrays into one
+ * by appending additional tabs to the end of the original array
+ * @param {string[]} tabOrder Tab order provided by the server
+ * @param {string[]} otherTabOrder Tab order provided by configuration
+ * @return {string[]}
+ */
+export function mergeTabOrder (tabOrder, otherTabOrder, tabs) {
+  for (const tabConfig of otherTabOrder) {
+    if (tabOrder.includes(tabConfig)) {
+      continue;
+    }
+    // isFirst should be an override to dynamic tab ordering.
+    if (tabs[tabConfig] && tabs[tabConfig].isFirst) {
+      tabOrder.unshift(tabConfig);
+    } else {
+      tabOrder.push(tabConfig);
+    }
+  }
+  return tabOrder;
+}
+
+export function getTabOrder (tabsConfig, dataTabOrder, urlParams) {
+  let tabOrder = getDefaultTabOrder(tabsConfig, getUrlParams(urlParams));
+  // We want to persist the params from the existing URL to the new
+  // URLS we create.
+  if (tabOrder && dataTabOrder) {
+    tabOrder = mergeTabOrder(dataTabOrder, tabOrder, tabsConfig);
+  }
+  return tabOrder;
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Thu May 27 2021 09:23:11 GMT-0400 (Eastern Daylight Time) +
+ + + + + diff --git a/gulpfile.js b/gulpfile.js index f6c6175d0..2b3ef0f4d 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -21,3 +21,4 @@ exports.buildLocales = parallel( templates.buildLocales ); exports.extractTranslations = extractTranslations; +exports.templates = templates.default; diff --git a/package-lock.json b/package-lock.json index c2ae50d6f..c2273ca70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3767,6 +3767,71 @@ "kuler": "^2.0.0" } }, + "@eslint/eslintrc": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.2.tgz", + "integrity": "sha512-8nmGq/4ycLpIwzvhI4tNDmQztZ8sp+hI7cyG8i1nQDhkAbRzHpXPidRAHlNvCZQpJTKw5ItIpMw9RSToGF00mg==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "globals": { + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.9.0.tgz", + "integrity": "sha512-74/FduwI/JaIrr1H8e71UbDE+5x7pIPs1C2rrwC52SszOo043CsWOZEMW7o2Y58xwm9b+0RBKDxY5n2sUpEFxA==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } + } + }, "@gulp-sourcemaps/identity-map": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-1.0.2.tgz", @@ -5214,6 +5279,12 @@ "jest-diff": "^24.3.0" } }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "dev": true + }, "@types/lodash": { "version": "4.14.168", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz", @@ -5481,12 +5552,6 @@ "uri-js": "^4.2.2" } }, - "ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true - }, "align-text": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/align-text/-/align-text-1.0.2.tgz", @@ -5970,16 +6035,127 @@ "dev": true }, "array-includes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.2.tgz", - "integrity": "sha512-w2GspexNQpx+PutG3QpT437/BenZBj0M/MZGn5mzv/MofYqo0xmRHzn4lFsoDlWJ+THYsGJmFlW68WlDFx7VRw==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.3.tgz", + "integrity": "sha512-gcem1KlBU7c9rB+Rq8/3PPKsK2kjqeEBa3bD5kkQo4nYlOHQCJqIJFqBXDEfwaRuYTT4E+FxA9xez7Gf/e3Q7A==", "dev": true, "requires": { - "call-bind": "^1.0.0", + "call-bind": "^1.0.2", "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.1", - "get-intrinsic": "^1.0.1", + "es-abstract": "^1.18.0-next.2", + "get-intrinsic": "^1.1.1", "is-string": "^1.0.5" + }, + "dependencies": { + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "es-abstract": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.3.tgz", + "integrity": "sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "is-callable": "^1.2.3", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.3", + "is-string": "^1.0.6", + "object-inspect": "^1.10.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.1" + }, + "dependencies": { + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true + }, + "is-string": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz", + "integrity": "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==", + "dev": true + } + } + }, + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "is-callable": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", + "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==", + "dev": true + }, + "is-regex": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", + "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-symbols": "^1.0.2" + }, + "dependencies": { + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true + } + } + }, + "object-inspect": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz", + "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==", + "dev": true + }, + "string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + } } }, "array-initial": { @@ -6305,65 +6481,6 @@ } } }, - "babel-code-frame": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "esutils": "^2.0.2", - "js-tokens": "^3.0.2" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", - "dev": true - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, "babel-jest": { "version": "24.9.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-24.9.0.tgz", @@ -7288,12 +7405,6 @@ "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", "dev": true }, - "chardet": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", - "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", - "dev": true - }, "check-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", @@ -7539,12 +7650,6 @@ "safe-buffer": "^5.0.1" } }, - "circular-json": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", - "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", - "dev": true - }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -7591,15 +7696,6 @@ "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", "dev": true }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "dev": true, - "requires": { - "restore-cursor": "^2.0.0" - } - }, "cli-spinners": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.5.0.tgz", @@ -7687,12 +7783,6 @@ } } }, - "cli-width": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", - "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", - "dev": true - }, "clipboardy": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-1.2.3.tgz", @@ -8119,12 +8209,6 @@ "bluebird": "^3.1.1" } }, - "contains-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", - "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", - "dev": true - }, "content-disposition": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", @@ -8747,12 +8831,6 @@ "object-assign": "4.X" } }, - "debug-log": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/debug-log/-/debug-log-1.0.1.tgz", - "integrity": "sha1-IwdjLUwEOCuN+KMvcLiVBG1SdF8=", - "dev": true - }, "debuglog": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", @@ -8912,20 +8990,6 @@ } } }, - "deglob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/deglob/-/deglob-3.1.0.tgz", - "integrity": "sha512-al10l5QAYaM/PeuXkAr1Y9AQz0LCtWsnJG23pIgh44hDxHFOj36l6qvhfjnIWBYwZOqM1fXUFV9tkjL7JPdGvw==", - "dev": true, - "requires": { - "find-root": "^1.0.0", - "glob": "^7.0.5", - "ignore": "^5.0.0", - "pkg-config": "^1.1.0", - "run-parallel": "^1.1.2", - "uniq": "^1.0.1" - } - }, "del": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/del/-/del-5.1.0.tgz", @@ -9065,9 +9129,9 @@ "dev": true }, "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, "requires": { "esutils": "^2.0.2" @@ -9523,118 +9587,231 @@ } }, "eslint": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.4.0.tgz", - "integrity": "sha512-UIpL91XGex3qtL6qwyCQJar2j3osKxK9e3ano3OcGEIRM4oWIpCkDg9x95AXEC2wMs7PnxzOkPZ2gq+tsMS9yg==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.28.0.tgz", + "integrity": "sha512-UMfH0VSjP0G4p3EWirscJEQ/cHqnT/iuH6oNZOB94nBjWbMnhGEPxsZm1eyIW0C/9jLI0Fow4W5DXLjEI7mn1g==", "dev": true, "requires": { - "ajv": "^6.5.0", - "babel-code-frame": "^6.26.0", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", - "debug": "^3.1.0", - "doctrine": "^2.1.0", - "eslint-scope": "^4.0.0", - "eslint-utils": "^1.3.1", - "eslint-visitor-keys": "^1.0.0", - "espree": "^4.0.0", - "esquery": "^1.0.1", + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.2", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", "esutils": "^2.0.2", - "file-entry-cache": "^2.0.0", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", "functional-red-black-tree": "^1.0.1", - "glob": "^7.1.2", - "globals": "^11.7.0", - "ignore": "^4.0.2", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", - "inquirer": "^5.2.0", - "is-resolvable": "^1.1.0", - "js-yaml": "^3.11.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.5", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", "natural-compare": "^1.4.0", - "optionator": "^0.8.2", - "path-is-inside": "^1.0.2", - "pluralize": "^7.0.0", + "optionator": "^0.9.1", "progress": "^2.0.0", - "regexpp": "^2.0.0", - "require-uncached": "^1.0.3", - "semver": "^5.5.0", - "strip-ansi": "^4.0.0", - "strip-json-comments": "^2.0.1", - "table": "^4.0.3", - "text-table": "^0.2.0" + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" }, "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" + "ms": "2.1.2" + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" } }, + "globals": { + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.9.0.tgz", + "integrity": "sha512-74/FduwI/JaIrr1H8e71UbDE+5x7pIPs1C2rrwC52SszOo043CsWOZEMW7o2Y58xwm9b+0RBKDxY5n2sUpEFxA==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, "ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, "requires": { - "ansi-regex": "^3.0.0" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" } }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "dev": true, "requires": { - "isexe": "^2.0.0" + "lru-cache": "^6.0.0" } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true } } }, "eslint-config-semistandard": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-semistandard/-/eslint-config-semistandard-13.0.0.tgz", - "integrity": "sha512-ZuImKnf/9LeZjr6dtRJ0zEdQbjBwXu0PJR3wXJXoQeMooICMrYPyD70O1tIA9Ng+wutgLjB7UXvZOKYPvzHg+w==", + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/eslint-config-semistandard/-/eslint-config-semistandard-15.0.1.tgz", + "integrity": "sha512-sfV+qNBWKOmF0kZJll1VH5XqOAdTmLlhbOl9WKI11d2eMEe+Kicxnpm24PQWHOqAfk5pAWU2An0LjNCXKa4Usg==", "dev": true }, "eslint-config-standard": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-12.0.0.tgz", - "integrity": "sha512-COUz8FnXhqFitYj4DTqHzidjIL/t4mumGZto5c7DrBpvWoie+Sn3P4sLEzUGeYhRElWuFEf8K1S1EfvD1vixCQ==", - "dev": true - }, - "eslint-config-standard-jsx": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/eslint-config-standard-jsx/-/eslint-config-standard-jsx-6.0.2.tgz", - "integrity": "sha512-D+YWAoXw+2GIdbMBRAzWwr1ZtvnSf4n4yL0gKGg7ShUOGXkSOLerI17K4F6LdQMJPNMoWYqepzQD/fKY+tXNSg==", + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-16.0.3.tgz", + "integrity": "sha512-x4fmJL5hGqNJKGHSjnLdgA6U6h1YW/G2dW9fA+cyVur4SK6lyue8+UgNKWlZtUDTXvgKDD/Oa3GQjmB5kjtVvg==", "dev": true }, "eslint-import-resolver-node": { @@ -9665,24 +9842,15 @@ } }, "eslint-module-utils": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz", - "integrity": "sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.1.tgz", + "integrity": "sha512-ZXI9B8cxAJIH4nfkhTwcRTEAnrVfobYqwjWy/QMCZ8rHkZHFjf9yO4BzpiF9kCSfNlMG54eKigISHpX0+AaT4A==", "dev": true, "requires": { - "debug": "^2.6.9", + "debug": "^3.2.7", "pkg-dir": "^2.0.0" }, "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, "find-up": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", @@ -9702,12 +9870,6 @@ "path-exists": "^3.0.0" } }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, "p-limit": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", @@ -9750,33 +9912,48 @@ } }, "eslint-plugin-es": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-1.4.1.tgz", - "integrity": "sha512-5fa/gR2yR3NxQf+UXkeLeP8FBBl6tSgdrAz1+cF84v1FMM4twGwQoqTnn+QxFLcPOrF4pdKEJKDB/q9GoyJrCA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", + "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", "dev": true, "requires": { - "eslint-utils": "^1.4.2", - "regexpp": "^2.0.1" + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" } }, "eslint-plugin-import": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.14.0.tgz", - "integrity": "sha512-FpuRtniD/AY6sXByma2Wr0TXvXJ4nA/2/04VPlfpmUDPOpOY264x+ILiwnrk/k4RINgDAyFZByxqPUbSQ5YE7g==", + "version": "2.23.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.23.4.tgz", + "integrity": "sha512-6/wP8zZRsnQFiR3iaPFgh5ImVRM1WN5NUWfTIRqwOdeiGJlBcSk82o1FEVq8yXmy4lkIzTo7YhHCIxlU/2HyEQ==", "dev": true, "requires": { - "contains-path": "^0.1.0", - "debug": "^2.6.8", - "doctrine": "1.5.0", - "eslint-import-resolver-node": "^0.3.1", - "eslint-module-utils": "^2.2.0", - "has": "^1.0.1", - "lodash": "^4.17.4", - "minimatch": "^3.0.3", - "read-pkg-up": "^2.0.0", - "resolve": "^1.6.0" + "array-includes": "^3.1.3", + "array.prototype.flat": "^1.2.4", + "debug": "^2.6.9", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.4", + "eslint-module-utils": "^2.6.1", + "find-up": "^2.0.0", + "has": "^1.0.3", + "is-core-module": "^2.4.0", + "minimatch": "^3.0.4", + "object.values": "^1.1.3", + "pkg-up": "^2.0.0", + "read-pkg-up": "^3.0.0", + "resolve": "^1.20.0", + "tsconfig-paths": "^3.9.0" }, "dependencies": { + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -9787,13 +9964,44 @@ } }, "doctrine": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", - "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, "requires": { - "esutils": "^2.0.2", - "isarray": "^1.0.0" + "esutils": "^2.0.2" + } + }, + "es-abstract": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.3.tgz", + "integrity": "sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "is-callable": "^1.2.3", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.3", + "is-string": "^1.0.6", + "object-inspect": "^1.10.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.1" + }, + "dependencies": { + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true + } } }, "find-up": { @@ -9805,15 +10013,65 @@ "locate-path": "^2.0.0" } }, + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "is-callable": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", + "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==", + "dev": true + }, + "is-core-module": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", + "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-regex": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", + "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-symbols": "^1.0.2" + }, + "dependencies": { + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true + } + } + }, + "is-string": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz", + "integrity": "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==", + "dev": true + }, "load-json-file": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", - "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", "dev": true, "requires": { "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", + "parse-json": "^4.0.0", + "pify": "^3.0.0", "strip-bom": "^3.0.0" } }, @@ -9833,6 +10091,23 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, + "object-inspect": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz", + "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==", + "dev": true + }, + "object.values": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.4.tgz", + "integrity": "sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.2" + } + }, "p-limit": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", @@ -9857,49 +10132,85 @@ "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", "dev": true }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", "dev": true, "requires": { - "error-ex": "^1.2.0" + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz", + "integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=", + "dev": true, + "requires": { + "find-up": "^2.1.0" + } + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^3.0.0" } }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, - "path-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", - "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", "dev": true, "requires": { - "pify": "^2.0.0" + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" } }, - "read-pkg": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", - "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", "dev": true, "requires": { - "load-json-file": "^2.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^2.0.0" + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" } }, - "read-pkg-up": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", - "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", "dev": true, "requires": { - "find-up": "^2.0.0", - "read-pkg": "^2.0.0" + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" } }, "strip-bom": { @@ -9911,75 +10222,64 @@ } }, "eslint-plugin-node": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-7.0.1.tgz", - "integrity": "sha512-lfVw3TEqThwq0j2Ba/Ckn2ABdwmL5dkOgAux1rvOk6CO7A6yGyPI2+zIxN6FyNkp1X1X/BSvKOceD6mBWSj4Yw==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", + "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", "dev": true, "requires": { - "eslint-plugin-es": "^1.3.1", - "eslint-utils": "^1.3.1", - "ignore": "^4.0.2", + "eslint-plugin-es": "^3.0.0", + "eslint-utils": "^2.0.0", + "ignore": "^5.1.1", "minimatch": "^3.0.4", - "resolve": "^1.8.1", - "semver": "^5.5.0" + "resolve": "^1.10.1", + "semver": "^6.1.0" }, "dependencies": { - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true } } }, "eslint-plugin-promise": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.0.1.tgz", - "integrity": "sha512-Si16O0+Hqz1gDHsys6RtFRrW7cCTB6P7p3OJmKp3Y3dxpQE2qwOA7d3xnV+0mBmrPoi0RBnxlCKvqu70te6wjg==", - "dev": true - }, - "eslint-plugin-react": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.11.1.tgz", - "integrity": "sha512-cVVyMadRyW7qsIUh3FHp3u6QHNhOgVrLQYdQEB1bPWBsgbNCHdFAeNMquBMCcZJu59eNthX053L70l7gRt4SCw==", - "dev": true, - "requires": { - "array-includes": "^3.0.3", - "doctrine": "^2.1.0", - "has": "^1.0.3", - "jsx-ast-utils": "^2.0.1", - "prop-types": "^15.6.2" - } - }, - "eslint-plugin-standard": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-standard/-/eslint-plugin-standard-4.0.2.tgz", - "integrity": "sha512-nKptN8l7jksXkwFk++PhJB3cCDTcXOEyhISIN86Ue2feJ1LFyY3PrY3/xT2keXlJSY5bpmbiTG0f885/YKAvTA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-5.1.0.tgz", + "integrity": "sha512-NGmI6BH5L12pl7ScQHbg7tvtk4wPxxj8yPHH47NvSmMtFneC077PSeY3huFj06ZWZvtbfxSPt3RuOQD5XcR4ng==", "dev": true }, "eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "requires": { - "esrecurse": "^4.1.0", + "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", "dev": true, "requires": { "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } } }, "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", "dev": true }, "esm": { @@ -10006,20 +10306,26 @@ } }, "espree": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-4.1.0.tgz", - "integrity": "sha512-I5BycZW6FCVIub93TeVY1s7vjhP9CY6cXCznIRfiig7nRviKZYdRnj/sHEWC6A7WE9RDWOFq9+7OsWSYz8qv2w==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", "dev": true, "requires": { - "acorn": "^6.0.2", - "acorn-jsx": "^5.0.0", - "eslint-visitor-keys": "^1.0.0" + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" }, "dependencies": { "acorn": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", - "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", "dev": true } } @@ -10031,9 +10337,9 @@ "dev": true }, "esquery": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", - "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", "dev": true, "requires": { "estraverse": "^5.1.0" @@ -10373,17 +10679,6 @@ } } }, - "external-editor": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", - "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", - "dev": true, - "requires": { - "chardet": "^0.4.0", - "iconv-lite": "^0.4.17", - "tmp": "^0.0.33" - } - }, "extglob": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", @@ -10590,23 +10885,13 @@ "integrity": "sha512-aN3pcx/DSmtyoovUudctc8+6Hl4T+hI9GBBHLjA76jdZl7+b1sgh5g4k+u/GL3dTy1/pnYzKp69FpJ0OicE3Wg==", "dev": true }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, "file-entry-cache": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", - "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, "requires": { - "flat-cache": "^1.2.1", - "object-assign": "^4.0.1" + "flat-cache": "^3.0.4" } }, "file-type": { @@ -10687,12 +10972,6 @@ } } }, - "find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "dev": true - }, "find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -10881,26 +11160,13 @@ "dev": true }, "flat-cache": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.4.tgz", - "integrity": "sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", "dev": true, "requires": { - "circular-json": "^0.3.1", - "graceful-fs": "^4.1.2", - "rimraf": "~2.6.2", - "write": "^0.2.1" - }, - "dependencies": { - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } + "flatted": "^3.1.0", + "rimraf": "^3.0.2" } }, "flatted": { @@ -12221,6 +12487,12 @@ } } }, + "has-bigints": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", + "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", + "dev": true + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -12823,60 +13095,6 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, - "inquirer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-5.2.0.tgz", - "integrity": "sha512-E9BmnJbAKLPGonz0HeWHtbKf+EeSP93paWO3ZYoUpq/aowXvYGjjCSuashhXPpzbArIjBbji39THkxTz9ZeEUQ==", - "dev": true, - "requires": { - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.0", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^2.1.0", - "figures": "^2.0.0", - "lodash": "^4.3.0", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^5.5.2", - "string-width": "^2.1.0", - "strip-ansi": "^4.0.0", - "through": "^2.3.6" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, "interpret": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", @@ -12974,6 +13192,12 @@ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", "dev": true }, + "is-bigint": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz", + "integrity": "sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==", + "dev": true + }, "is-binary-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", @@ -14839,16 +15063,6 @@ "integrity": "sha512-/jsi/9C0S70zfkT/4UlKQa5E1xKurDnXcQizcww9JSR/Fv+uIbWM2btG+bFcL3iNoK9jIGS0ls9HWLr1iw0kFg==", "dev": true }, - "jsx-ast-utils": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz", - "integrity": "sha512-z1xSldJ6imESSzOjd3NNkieVJKRlKYSOtMG8SFyCj2FIrvSaSuli/WjpBkEzCBoR9bYYYFgqJw61Xhu7Lcgk+w==", - "dev": true, - "requires": { - "array-includes": "^3.1.1", - "object.assign": "^4.1.0" - } - }, "just-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.0.0.tgz", @@ -15376,6 +15590,12 @@ "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", "dev": true }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -15401,6 +15621,12 @@ "lodash._reinterpolate": "^3.0.0" } }, + "lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", + "dev": true + }, "lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -16222,12 +16448,6 @@ "integrity": "sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==", "dev": true }, - "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", - "dev": true - }, "nan": { "version": "2.14.2", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", @@ -17473,127 +17693,31 @@ "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", "dev": true }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, - "requires": { - "pinkie": "^2.0.0" - } - }, - "pirates": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", - "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", - "dev": true, - "requires": { - "node-modules-regexp": "^1.0.0" - } - }, - "pixelmatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-4.0.2.tgz", - "integrity": "sha1-j0fc7FARtHe2fbA8JDvB8wheiFQ=", - "dev": true, - "requires": { - "pngjs": "^3.0.0" - } - }, - "pkg-conf": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-2.1.0.tgz", - "integrity": "sha1-ISZRTKbyq/69FoWW3xi6V4Z/AFg=", - "dev": true, - "requires": { - "find-up": "^2.0.0", - "load-json-file": "^4.0.0" - }, - "dependencies": { - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - } + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" } }, - "pkg-config": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pkg-config/-/pkg-config-1.1.1.tgz", - "integrity": "sha1-VX7yLXPaPIg3EHdmxS6tq94pj+Q=", + "pirates": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", + "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", + "dev": true, + "requires": { + "node-modules-regexp": "^1.0.0" + } + }, + "pixelmatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-4.0.2.tgz", + "integrity": "sha1-j0fc7FARtHe2fbA8JDvB8wheiFQ=", "dev": true, "requires": { - "debug-log": "^1.0.0", - "find-root": "^1.0.0", - "xtend": "^4.0.1" + "pngjs": "^3.0.0" } }, "pkg-dir": { @@ -17707,12 +17831,6 @@ "resolved": "https://registry.npmjs.org/plural-forms/-/plural-forms-0.5.2.tgz", "integrity": "sha512-Tcne7cxK2behLyvEj7iqdfJjr4ji9s9OBxrD/Q/GUHrSP3o2UbwKI0fldQ+Hd7DP6+6RWhKkbpQps5DWvkij/Q==" }, - "pluralize": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", - "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==", - "dev": true - }, "pn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", @@ -19104,17 +19222,6 @@ "sisteransi": "^1.0.5" } }, - "prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "dev": true, - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.8.1" - } - }, "proxy-addr": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", @@ -19692,9 +19799,9 @@ } }, "regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", "dev": true }, "regexpu-core": { @@ -19954,45 +20061,18 @@ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "dev": true }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, "require-main-filename": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", "dev": true }, - "require-uncached": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", - "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", - "dev": true, - "requires": { - "caller-path": "^0.1.0", - "resolve-from": "^1.0.0" - }, - "dependencies": { - "caller-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", - "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", - "dev": true, - "requires": { - "callsites": "^0.2.0" - } - }, - "callsites": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", - "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", - "dev": true - }, - "resolve-from": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", - "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", - "dev": true - } - } - }, "requizzle": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.3.tgz", @@ -20057,16 +20137,6 @@ "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "dev": true, - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - } - }, "ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", @@ -20251,15 +20321,6 @@ "integrity": "sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw==", "dev": true }, - "rxjs": { - "version": "5.5.12", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.12.tgz", - "integrity": "sha512-xx2itnL5sBbqeeiVgNPVuQQ1nC8Jp2WfNJhXWHmElW9YmrpS9UVnNzhP3EH3HFqexO5Tlp8GhYY+WEcqcVMvGw==", - "dev": true, - "requires": { - "symbol-observable": "1.0.1" - } - }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -20411,6 +20472,80 @@ "truncate-utf8-bytes": "^1.0.0" } }, + "sass": { + "version": "1.34.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.34.0.tgz", + "integrity": "sha512-rHEN0BscqjUYuomUEaqq3BMgsXqQfkcMVR7UhscsAVub0/spUrZGBMxQXFS2kfiDsPLZw5yuU9iJEFNC2x38Qw==", + "dev": true, + "requires": { + "chokidar": ">=3.0.0 <4.0.0" + }, + "dependencies": { + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "chokidar": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", + "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.3.1", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" + } + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + } + } + }, "sass-graph": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.5.tgz", @@ -20542,9 +20677,9 @@ } }, "y18n": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", - "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "dev": true }, "yargs": { @@ -20604,24 +20739,6 @@ } } }, - "semistandard": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/semistandard/-/semistandard-13.0.1.tgz", - "integrity": "sha512-2GkuX4BsoMEYoufJYRz8/ERbYDfgOO3yP29IBaoXtxl202azlkV1MsFyoSFiM6GBUfL7MSUxSy38KfM9oDAE2g==", - "dev": true, - "requires": { - "eslint": "~5.4.0", - "eslint-config-semistandard": "13.0.0", - "eslint-config-standard": "12.0.0", - "eslint-config-standard-jsx": "6.0.2", - "eslint-plugin-import": "~2.14.0", - "eslint-plugin-node": "~7.0.1", - "eslint-plugin-promise": "~4.0.0", - "eslint-plugin-react": "~7.11.1", - "eslint-plugin-standard": "~4.0.0", - "standard-engine": "~10.0.0" - } - }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -21046,18 +21163,44 @@ "dev": true }, "slice-ansi": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", - "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "requires": { - "is-fullwidth-code-point": "^2.0.0" + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" }, "dependencies": { - "is-fullwidth-code-point": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "astral-regex": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true } } @@ -21382,26 +21525,6 @@ "integrity": "sha1-M6qE8Rd6VUjIk1Uzy/6zQgl19aQ=", "dev": true }, - "standard-engine": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/standard-engine/-/standard-engine-10.0.0.tgz", - "integrity": "sha512-91BjmzIRZbFmyOY73R6vaDd/7nw5qDWsfpJW5/N+BCXFgmvreyfrRg7oBSu4ihL0gFGXfnwCImJm6j+sZDQQyw==", - "dev": true, - "requires": { - "deglob": "^3.0.0", - "get-stdin": "^6.0.0", - "minimist": "^1.1.0", - "pkg-conf": "^2.0.0" - }, - "dependencies": { - "get-stdin": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", - "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", - "dev": true - } - } - }, "static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -22281,12 +22404,6 @@ "util.promisify": "~1.0.0" } }, - "symbol-observable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", - "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=", - "dev": true - }, "symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -22294,49 +22411,36 @@ "dev": true }, "table": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/table/-/table-4.0.3.tgz", - "integrity": "sha512-S7rnFITmBH1EnyKcvxBh1LjYeQMmnZtCXSEbHcH6S0NoKit24ZuFO/T1vDcLdYsLQkM188PVVhQmzKIuThNkKg==", + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/table/-/table-6.7.1.tgz", + "integrity": "sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg==", "dev": true, "requires": { - "ajv": "^6.0.1", - "ajv-keywords": "^3.0.0", - "chalk": "^2.1.0", - "lodash": "^4.17.4", - "slice-ansi": "1.0.0", - "string-width": "^2.1.1" + "ajv": "^8.0.1", + "lodash.clonedeep": "^4.5.0", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0" }, "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "ajv": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.0.tgz", + "integrity": "sha512-cnUG4NSBiM4YFBxgZIj/In3/6KX+rQ2l2YPRVcvAMQGWEPKuXoPIhxzwqh31jA3IPbI4qEOp/5ILI4ynioXsGQ==", "dev": true, "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" } }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true } } }, @@ -23830,6 +23934,35 @@ "integrity": "sha1-OTvnMKlEb9Hq1tpZoBQwjzbCics=", "dev": true }, + "tsconfig-paths": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", + "integrity": "sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + } + } + }, "tslib": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", @@ -23922,6 +24055,26 @@ "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==", "dev": true }, + "unbox-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", + "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has-bigints": "^1.0.1", + "has-symbols": "^1.0.2", + "which-boxed-primitive": "^1.0.2" + }, + "dependencies": { + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true + } + } + }, "unbzip2-stream": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", @@ -24650,6 +24803,19 @@ "isexe": "^2.0.0" } }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, "which-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", @@ -24870,15 +25036,6 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, - "write": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", - "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", - "dev": true, - "requires": { - "mkdirp": "^0.5.1" - } - }, "write-file-atomic": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.1.tgz", diff --git a/package.json b/package.json index 5b071ae87..264809312 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,9 @@ "javascript", "vanilla" ], + "files": [ + "dist" + ], "dependencies": { "@mapbox/mapbox-gl-language": "^0.10.1", "@yext/answers-core": "^1.2.0-alpha.0", @@ -53,6 +56,12 @@ "cssnano": "^4.1.10", "del": "^5.1.0", "enzyme": "^3.11.0", + "eslint": "^7.28.0", + "eslint-config-semistandard": "^15.0.1", + "eslint-config-standard": "^16.0.3", + "eslint-plugin-import": "^2.23.4", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^5.1.0", "fs-extra": "^9.0.1", "generate-license-file": "^1.1.0", "gettext-extractor": "^3.5.2", @@ -65,7 +74,7 @@ "gulp-rename": "^1.4.0", "gulp-replace": "^1.0.0", "gulp-rollup-lightweight": "^1.0.10", - "gulp-sass": "^4.0.2", + "gulp-sass": "^4.1.0", "gulp-uglify-es": "^2.0.0", "gulp-umd": "^2.0.0", "gulp-wrap": "^0.14.0", @@ -73,7 +82,6 @@ "i18next-conv": "^10.0.2", "jest": "^24.8.0", "jsdoc": "^3.6.3", - "node-sass": "^4.12.0", "postcss-pxtorem": "4.0.1", "regenerator-runtime": "^0.13.7", "rollup": "^1.4.1", @@ -81,7 +89,7 @@ "rollup-plugin-commonjs": "^9.2.1", "rollup-plugin-node-builtins": "^2.1.2", "rollup-plugin-node-resolve": "^4.0.1", - "semistandard": "^13.0.1", + "sass": "^1.34.0", "serve": "^11.3.0", "size-limit": "^4.9.0", "stylelint": "^13.7.1", @@ -96,11 +104,11 @@ "build-locales": "gulp buildLocales && size-limit", "dev": "gulp dev", "docs": "jsdoc -R README.md -d docs/ -r src/", - "lint": "semistandard", - "test": "semistandard && stylelint src/**/*.scss && jest", + "lint": "eslint .", + "test": "eslint . && stylelint src/**/*.scss && jest", "acceptance": "testcafe safari,chrome tests/acceptance/acceptancesuites/*.js", "size": "size-limit", - "fix": "semistandard --fix", + "fix": "eslint . --fix", "extract-translations": "gulp extractTranslations", "prepublishOnly": "./conf/npm/prepublishOnly.sh", "postpublish": "./conf/npm/postpublish.sh", @@ -108,6 +116,10 @@ }, "jest": { "bail": 0, + "collectCoverageFrom": [ + "/src/**/*.js", + "/conf/**/*.js" + ], "verbose": true, "setupFilesAfterEnv": [ "./tests/setup/setup.js" @@ -125,25 +137,6 @@ "**/tests/conf/**/*.js" ] }, - "semistandard": { - "globals": [ - "beforeEach", - "beforeAll", - "describe", - "it", - "expect", - "jest", - "DOMParser", - "ytag", - "test", - "fixture", - "CustomEvent", - "ANSWERS" - ], - "ignore": [ - "docs/" - ] - }, "percy": { "agent": { "agent-discovery": { diff --git a/sample-app/.gitignore b/sample-app/.gitignore deleted file mode 100644 index 9b8262d7f..000000000 --- a/sample-app/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -credentials.json -*.html diff --git a/sample-app/README.md b/sample-app/README.md deleted file mode 100644 index b0facdb97..000000000 --- a/sample-app/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# Answers Sample App - -## Install -``` -npm install -``` - -## Add credentials -`credentials.json`: -```json -{ - "apiKey": "0123456789abcdefg", - "mapApiKey": "0123456789abcdefg" -} -``` - -## Run -``` -npm start -``` -or -``` -npm start [config_file] -``` -Open `index.html` in your browser of choice diff --git a/sample-app/config.json b/sample-app/config.json deleted file mode 100644 index a353e2183..000000000 --- a/sample-app/config.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "answersKey": "deepakyextanswersconfig", - "mapProvider": "mapbox", - "verticals": { - "offices": { - "map": true - }, - "jobs": {} - } -} diff --git a/sample-app/index.js b/sample-app/index.js deleted file mode 100644 index de74e53e0..000000000 --- a/sample-app/index.js +++ /dev/null @@ -1,68 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const util = require('util'); -const writeFile = util.promisify(fs.writeFile); - -const Handlebars = require('handlebars'); -const metadataTemplate = Handlebars.compile( - fs.readFileSync('./metadata.hbs', 'utf8') -); -const universalTemplate = Handlebars.compile( - fs.readFileSync('./universal.hbs', 'utf8') -); -const verticalTemplate = Handlebars.compile( - fs.readFileSync('./vertical.hbs', 'utf8') -); -Handlebars.registerPartial('metadata', metadataTemplate); - -const bundleUrl = '../dist/answers-modern.js'; -const templateUrl = '../dist/answerstemplates.compiled.min.js'; -const cssUrl = '../dist/answers.css'; - -const credentials = require('./credentials.json'); -const configFile = process.argv[2] - ? path.join(process.cwd(), process.argv[2]) - : './sample-config.json'; -const siteConfig = require(configFile); -const config = Object.assign({}, credentials, siteConfig, { - templateUrl -}); - -function main () { - writeUniversalHtml(); - - for (const verticalKey of Object.keys(config.verticals)) { - writeVerticalHtml(verticalKey); - } -} - -function writeUniversalHtml () { - const html = universalTemplate({ - bundleUrl, - cssUrl, - title: 'Universal Search Experience Test', - configJson: new Handlebars.SafeString(JSON.stringify(config)) - }); - writeFile('index.html', html) - .catch(console.error); -} - -function writeVerticalHtml (verticalKey) { - const configWithVertical = Object.assign({}, config, { - verticalKey - }); - const html = verticalTemplate({ - bundleUrl, - cssUrl, - title: `${capitalize(verticalKey)} Experience Test`, - configJson: new Handlebars.SafeString(JSON.stringify(configWithVertical)) - }); - writeFile(`${verticalKey}.html`, html) - .catch(console.error); -} - -function capitalize (s) { - return s.charAt(0).toUpperCase() + s.slice(1); -} - -main(); diff --git a/sample-app/metadata.hbs b/sample-app/metadata.hbs deleted file mode 100644 index 2d58719b2..000000000 --- a/sample-app/metadata.hbs +++ /dev/null @@ -1,12 +0,0 @@ - - - - {{title}} - - - - - - diff --git a/sample-app/package.json b/sample-app/package.json deleted file mode 100644 index c76ea2139..000000000 --- a/sample-app/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "answers-sample-app", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "buildLib": "npm run -C .. build", - "buildHtml": "node index.js", - "start": "npm run buildLib && npm run buildHtml" - }, - "author": "Adrian Mullings", - "dependencies": { - "handlebars": "^4.4.3" - } -} diff --git a/sample-app/public/app.js b/sample-app/public/app.js deleted file mode 100644 index 7511e015c..000000000 --- a/sample-app/public/app.js +++ /dev/null @@ -1,115 +0,0 @@ -/* global ANSWERS:readonly */ - -const config = JSON.parse(document.getElementById('config').textContent); - -window.initAnswers = async function initAnswers () { - const { - mock, - templateUrl, - apiKey, - experienceKey, - verticalKey, - mapProvider, - mapApiKey, - businessId, - search, - sessionTrackingEnabled, - locale - } = config; - const verticalConfig = config.verticals[verticalKey]; - const mapConfig = { - includeMap: true, - mapConfig: { - mapProvider, - apiKey: mapApiKey - } - }; - const searchConfig = Object.assign( - {}, - search, - verticalConfig ? verticalConfig.search : {}, - verticalKey ? { verticalKey } : {}); - const navigationConfig = { - tabs: [ - { - label: 'Home', - url: 'index.html', - isFirst: true, - isActive: !verticalKey - }, - ...Object.keys(config.verticals).map(v => ({ - configId: v, - label: v, - url: `${v}.html`, - isActive: verticalKey === v - })) - ] - }; - - ANSWERS.init({ - mock, - templateUrl, - apiKey, - experienceKey, - businessId, - search: searchConfig, - navigation: navigationConfig, - sessionTrackingEnabled: sessionTrackingEnabled, - locale: locale, - onReady: function () { - ANSWERS.addComponent('Navigation', { - container: '.navigation-container' - }); - - ANSWERS.addComponent('SearchBar', { - container: '.search-container', - autoFocus: true, - verticalKey, - allowEmptySearch: verticalConfig && verticalConfig.allowEmptySearch - }); - - if (!verticalKey) { - ANSWERS.addComponent('DirectAnswer', { - container: '.direct-answer-container' - }); - - ANSWERS.addComponent('UniversalResults', { - container: '.universal-results-container', - config: Object.fromEntries( - Object.entries(config.verticals) - .filter(([ key, config ]) => config.map) - .map(([ key, config ]) => [ key, mapConfig ]) - ) - }); - - ANSWERS.addComponent('QASubmission', { - container: '.question-submission-container', - entityId: 13171501 - }); - } else { - ANSWERS.addComponent('VerticalResults', { - container: '.results-container', - ...(verticalConfig.map ? mapConfig : {}) - }); - - ANSWERS.addComponent('FilterSearch', { - container: '.filter-search-container', - verticalKey - }); - - if (verticalConfig.filters) { - ANSWERS.addComponent('FilterBox', { - container: '.filters-container', - verticalKey, - filters: verticalConfig.filters - }); - } else { - ANSWERS.addComponent('Facets', { - container: '.facets-container', - verticalKey - }); - } - } - } - }); -}; diff --git a/sample-app/public/style.css b/sample-app/public/style.css deleted file mode 100644 index 2905efd41..000000000 --- a/sample-app/public/style.css +++ /dev/null @@ -1,14 +0,0 @@ -.search { - display: flex; - max-width: 900px; - flex-flow: row wrap; -} - -main { - width: min-content; - flex: 100000 1 auto; -} - -.sidebar { - flex: 1 0 15rem; -} diff --git a/sample-app/sample-config.json b/sample-app/sample-config.json deleted file mode 100644 index 2970f099b..000000000 --- a/sample-app/sample-config.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "mock": false, - "experienceKey": "deepakyextanswersconfig", - "mapProvider": "mapbox", - "businessId": "919871", - "sessionTrackingEnabled": true, - "locale": "en", - "verticals": { - "leadership": { - "search": { - "defaultInitialSearch": "executives" - }, - "filters": [ - { - "type": "FilterOptions", - "control": "singleoption", - "title": "University", - "options": [ - { - "label": "Cornell", - "field": "c_education.institutionName", - "value": "Cornell" - }, - { - "label": "Duke", - "field": "c_education.institutionName", - "value": "Duke" - }, - { - "label": "Princeton", - "field": "c_education.institutionName", - "value": "Princeton" - } - ] - }, - { - "type": "FilterOptions", - "control": "multioption", - "title": "Title", - "options": [ - { - "label": "Chief Executive Officer", - "field": "c_title", - "value": "CEO & Founder" - }, - { - "label": "Chief Revenue Officer", - "field": "c_title", - "value": "President & Chief Revenue Officer" - }, - { - "label": "Chief Technology Officer", - "field": "c_title", - "value": "Chief Technology Officer" - } - ] - } - ] - }, - "offices": { - "map": true, - "search": { - "defaultInitialSearch": "New York" - }, - "allowEmptySearch": true - }, - "events": { - "search": { - "defaultInitialSearch": "Answers" - } - }, - "speakers": { - "search": { - "defaultInitialSearch": "" - }, - "allowEmptySearch": true - }, - "jobs": { - "search": { - "defaultInitialSearch": "" - } - }, - "packages": {}, - "app_directory_partners": {}, - "publishers": {}, - "faqs": {} - } -} diff --git a/sample-app/universal.hbs b/sample-app/universal.hbs deleted file mode 100644 index e13bd7680..000000000 --- a/sample-app/universal.hbs +++ /dev/null @@ -1,15 +0,0 @@ - - -{{> metadata }} - - - - - diff --git a/sample-app/vertical.hbs b/sample-app/vertical.hbs deleted file mode 100644 index 640bdc464..000000000 --- a/sample-app/vertical.hbs +++ /dev/null @@ -1,18 +0,0 @@ - - -{{> metadata }} - - - - - diff --git a/src/answers-umd.js b/src/answers-umd.js index b7d12faf7..b3f78321a 100644 --- a/src/answers-umd.js +++ b/src/answers-umd.js @@ -15,7 +15,6 @@ import ErrorReporter from './core/errors/errorreporter'; import ConsoleErrorReporter from './core/errors/consoleerrorreporter'; import { AnalyticsReporter, NoopAnalyticsReporter } from './core'; import Storage from './core/storage/storage'; -import { AnswersComponentError } from './core/errors/errors'; import AnalyticsEvent from './core/analytics/analyticsevent'; import StorageKeys from './core/storage/storagekeys'; import QueryTriggers from './core/models/querytriggers'; @@ -23,7 +22,6 @@ import SearchConfig from './core/models/searchconfig'; import ComponentManager from './ui/components/componentmanager'; import VerticalPagesConfig from './core/models/verticalpagesconfig'; import { SANDBOX, PRODUCTION, LOCALE, QUERY_SOURCE } from './core/constants'; -import MasterSwitchApi from './core/utils/masterswitchapi'; import RichTextFormatter from './core/utils/richtextformatter'; import { isValidContext } from './core/utils/apicontext'; import FilterNodeFactory from './core/filters/filternodefactory'; @@ -121,12 +119,6 @@ class Answers { * @private */ this._analyticsReporterService = null; - - /** - * @type {boolean} - * @private - */ - this._disabledByMasterSwitch = false; } static setInstance (instance) { @@ -149,7 +141,7 @@ class Answers { } }, resetListener: data => { - let query = data.get(StorageKeys.QUERY); + const query = data.get(StorageKeys.QUERY); const hasQuery = query || query === ''; this.core.storage.delete(StorageKeys.PERSISTED_LOCATION_RADIUS); this.core.storage.delete(StorageKeys.PERSISTED_FILTER); @@ -243,10 +235,6 @@ class Answers { parsedConfig.verticalPages = new VerticalPagesConfig(parsedConfig.verticalPages); const storage = this._initStorage(parsedConfig); - this._masterSwitchApi = statusPage - ? new MasterSwitchApi({ apiKey: parsedConfig.apiKey, ...statusPage }, storage) - : MasterSwitchApi.from(parsedConfig.apiKey, parsedConfig.experienceKey, storage); - this._services = parsedConfig.mock ? getMockServices() : getServices(parsedConfig, storage); @@ -306,9 +294,6 @@ class Answers { const asyncDeps = this._loadAsyncDependencies(parsedConfig); return asyncDeps.finally(() => { - if (this._disabledByMasterSwitch) { - throw new Error('MasterSwitchApi determined the front-end should be disabled'); - } this._onReady(); if (!this.components.getActiveComponent(SearchComponent.type)) { this._initQueryUpdateListener(parsedConfig.search); @@ -353,8 +338,7 @@ class Answers { _loadAsyncDependencies (parsedConfig) { const loadTemplates = this._loadTemplates(parsedConfig); const ponyfillCssVariables = this._handlePonyfillCssVariables(parsedConfig.disableCssVariablesPonyfill); - const masterSwitch = this._checkMasterSwitch(); - return Promise.all([loadTemplates, ponyfillCssVariables, masterSwitch]); + return Promise.all([loadTemplates, ponyfillCssVariables]); } _loadTemplates ({ useTemplates, templateBundle }) { @@ -373,19 +357,6 @@ class Answers { } } - _checkMasterSwitch () { - window.performance.mark('yext.answers.statusStart'); - const handleFulfilledMasterSwitch = (isDisabled) => { - this._disabledByMasterSwitch = isDisabled; - }; - const handleRejectedMasterSwitch = () => { - this._disabledByMasterSwitch = false; - }; - return this._masterSwitchApi.isDisabled() - .then(handleFulfilledMasterSwitch, handleRejectedMasterSwitch) - .finally(() => window.performance.mark('yext.answers.statusEnd')); - } - domReady (cb) { DOM.onReady(cb); } @@ -461,7 +432,7 @@ class Answers { try { this.components.create(type, opts).mount(); } catch (e) { - throw new AnswersComponentError('Failed to add component', type, e); + console.error('Failed to add component', type, '\n\n', e); } return this; } diff --git a/src/core/core.js b/src/core/core.js index 6e8e8a6bb..0741fbf90 100644 --- a/src/core/core.js +++ b/src/core/core.js @@ -724,17 +724,17 @@ export default class Core { * to use StorageKeys.VERTICAL_PAGES_CONFIG */ _getUrls (query) { - let nav = this._componentManager.getActiveComponent('Navigation'); + const nav = this._componentManager.getActiveComponent('Navigation'); if (!nav) { return undefined; } - let tabs = nav.getState('tabs'); - let urls = {}; + const tabs = nav.getState('tabs'); + const urls = {}; if (tabs && Array.isArray(tabs)) { for (let i = 0; i < tabs.length; i++) { - let params = new SearchParams(tabs[i].url.split('?')[1]); + const params = new SearchParams(tabs[i].url.split('?')[1]); params.set('query', query); let url = tabs[i].baseUrl; diff --git a/src/core/errors/errorreporter.js b/src/core/errors/errorreporter.js index 5c7364eb7..9714a447c 100644 --- a/src/core/errors/errorreporter.js +++ b/src/core/errors/errorreporter.js @@ -90,10 +90,10 @@ export default class ErrorReporter { version: 20190301, environment: this.environment, params: { - 'libVersion': LIB_VERSION, - 'experienceVersion': this.experienceVersion, - 'experienceKey': this.experienceKey, - 'error': err.toJson() + libVersion: LIB_VERSION, + experienceVersion: this.experienceVersion, + experienceKey: this.experienceKey, + error: err.toJson() } }; const request = new ApiRequest(requestConfig, this.storage); diff --git a/src/core/errors/errors.js b/src/core/errors/errors.js index f130ca633..cf5cc4187 100644 --- a/src/core/errors/errors.js +++ b/src/core/errors/errors.js @@ -12,7 +12,7 @@ * 4XX errors: Core errors */ export class AnswersBaseError extends Error { - constructor (errorCode, message, boundary = 'unknown', causedBy) { + constructor (errorCode, message, boundary = 'unknown', causedBy = undefined) { super(message); this.errorCode = errorCode; this.errorMessage = message; diff --git a/src/core/eventemitter/eventemitter.js b/src/core/eventemitter/eventemitter.js index ed4d3b1bb..8a61834fc 100644 --- a/src/core/eventemitter/eventemitter.js +++ b/src/core/eventemitter/eventemitter.js @@ -65,13 +65,13 @@ export default class EventEmitter { * @param {Object} data the data to send along to the subscribers */ emit (evt, data) { - let listeners = this._listeners[evt]; + const listeners = this._listeners[evt]; if (listeners === undefined) { return; } // Invoke each of all the listener handlers and remove the ones that should fire only once. - let keep = []; + const keep = []; for (let i = 0; i < listeners.length; i++) { listeners[i].cb(data); if (listeners[i].once === true) { diff --git a/src/core/filters/filterregistry.js b/src/core/filters/filterregistry.js index b572425ed..d08b81b1a 100644 --- a/src/core/filters/filterregistry.js +++ b/src/core/filters/filterregistry.js @@ -115,9 +115,9 @@ export default class FilterRegistry { return filters.length === 1 ? filters[0] : { - filters: filters, - combinator: combinator - }; + filters: filters, + combinator: combinator + }; } /** diff --git a/src/core/filters/simplefilternode.js b/src/core/filters/simplefilternode.js index cdf96179c..58b455fde 100644 --- a/src/core/filters/simplefilternode.js +++ b/src/core/filters/simplefilternode.js @@ -95,7 +95,7 @@ export default class SimpleFilterNode extends FilterNode { return false; } return thisMatchers.every(m => - otherMatchersToValues.hasOwnProperty(m) && + m in otherMatchersToValues && otherMatchersToValues[m] === thisMatchersToValues[m] ); } diff --git a/src/core/http/apirequest.js b/src/core/http/apirequest.js index d43e00b83..7b0072d48 100644 --- a/src/core/http/apirequest.js +++ b/src/core/http/apirequest.js @@ -14,7 +14,7 @@ import { getLiveApiUrl } from '../utils/urlutils'; export default class ApiRequest { // TODO (tmeyer): Create an ApiService interface and pass an implementation to the current // consumers of ApiRequest as a dependency. - constructor (opts = {}, storage) { + constructor (opts = {}, storage = undefined) { /** * An abstraction used for making network request and handling errors * @type {HttpRequester} @@ -105,15 +105,15 @@ export default class ApiRequest { * @private */ baseParams () { - let baseParams = { - 'v': this._version, - 'api_key': this._apiKey, - 'jsLibVersion': LIB_VERSION, - 'sessionTrackingEnabled': this._storage.get(StorageKeys.SESSIONS_OPT_IN).value + const baseParams = { + v: this._version, + api_key: this._apiKey, + jsLibVersion: LIB_VERSION, + sessionTrackingEnabled: this._storage.get(StorageKeys.SESSIONS_OPT_IN).value }; const urlParams = new SearchParams(this._storage.getCurrentStateUrlMerged()); if (urlParams.has('beta')) { - baseParams['beta'] = urlParams.get('beta'); + baseParams.beta = urlParams.get('beta'); } return baseParams; diff --git a/src/core/http/httprequester.js b/src/core/http/httprequester.js index 536ba98be..270818eaa 100644 --- a/src/core/http/httprequester.js +++ b/src/core/http/httprequester.js @@ -50,8 +50,8 @@ export default class HttpRequester { request (method, url, opts) { const reqArgs = Object.assign({}, { - 'method': method, - 'credentials': 'include' + method: method, + credentials: 'include' }, opts); return this._fetch(url, reqArgs); @@ -89,9 +89,9 @@ export default class HttpRequester { return window.navigator.sendBeacon(url, data); } - var event = window.event && window.event.type; - var sync = event === 'unload' || event === 'beforeunload'; - var xhr = ('XMLHttpRequest' in window) ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP'); + const event = window.event && window.event.type; + const sync = event === 'unload' || event === 'beforeunload'; + const xhr = ('XMLHttpRequest' in window) ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP'); xhr.open('POST', url, !sync); xhr.setRequestHeader('Accept', '*/*'); if (typeof data === 'string') { @@ -113,7 +113,7 @@ export default class HttpRequester { let hasParam = url.indexOf('?') > -1; let searchQuery = ''; - for (let key in params) { + for (const key in params) { if (!hasParam) { hasParam = true; searchQuery += '?'; diff --git a/src/core/models/facet.js b/src/core/models/facet.js index 2f74e77a4..9e209441d 100644 --- a/src/core/models/facet.js +++ b/src/core/models/facet.js @@ -46,15 +46,15 @@ export default class Facet { */ static fromCore (coreFacets = []) { const facets = coreFacets.map(f => ({ - label: f['displayName'], - fieldId: f['fieldId'], + label: f.displayName, + fieldId: f.fieldId, options: f.options.map(o => ({ - label: o['displayName'], - countLabel: o['count'], - selected: o['selected'], + label: o.displayName, + countLabel: o.count, + selected: o.selected, filter: { - [f['fieldId']]: { - [o['matcher']]: o['value'] + [f.fieldId]: { + [o.matcher]: o.value } } })) diff --git a/src/core/models/filter.js b/src/core/models/filter.js index 4e4bef708..74b953b90 100644 --- a/src/core/models/filter.js +++ b/src/core/models/filter.js @@ -94,7 +94,7 @@ export default class Filter { */ static or (...filters) { return new Filter({ - [ FilterCombinators.OR ]: filters + [FilterCombinators.OR]: filters }); } @@ -105,7 +105,7 @@ export default class Filter { */ static and (...filters) { return new Filter({ - [ FilterCombinators.AND ]: filters + [FilterCombinators.AND]: filters }); } diff --git a/src/core/models/highlightedvalue.js b/src/core/models/highlightedvalue.js index 33ffaba4c..50d5c959d 100644 --- a/src/core/models/highlightedvalue.js +++ b/src/core/models/highlightedvalue.js @@ -115,8 +115,8 @@ export default class HighlightedValue { } for (let j = 0; j < highlightedSubstrings.length; j++) { - let start = Number(highlightedSubstrings[j].offset); - let end = start + highlightedSubstrings[j].length; + const start = Number(highlightedSubstrings[j].offset); + const end = start + highlightedSubstrings[j].length; highlightedValue += [ transformFunction(val.slice(nextStart, start)), diff --git a/src/core/models/navigation.js b/src/core/models/navigation.js index e1c7bfaac..b46f4b69f 100644 --- a/src/core/models/navigation.js +++ b/src/core/models/navigation.js @@ -7,7 +7,7 @@ export default class Navigation { } static from (modules) { - let nav = []; + const nav = []; if (!modules || !Array.isArray(modules)) { return nav; } diff --git a/src/core/models/questionsubmission.js b/src/core/models/questionsubmission.js index dc4c36f60..92ea071e7 100644 --- a/src/core/models/questionsubmission.js +++ b/src/core/models/questionsubmission.js @@ -5,7 +5,7 @@ * to power the QuestionSubmission component */ export default class QuestionSubmission { - constructor (question = {}, errors) { + constructor (question = {}, errors = null) { /** * The author of the question * @type {string} diff --git a/src/core/models/section.js b/src/core/models/section.js index 69edc6cbf..859f64712 100644 --- a/src/core/models/section.js +++ b/src/core/models/section.js @@ -3,7 +3,7 @@ import SearchStates from '../storage/searchstates'; export default class Section { - constructor (data = {}, url, resultsContext) { + constructor (data = {}, url = null, resultsContext = undefined) { this.searchState = SearchStates.SEARCH_COMPLETE; this.verticalConfigId = data.verticalConfigId || null; this.resultsCount = data.resultsCount || 0; @@ -21,12 +21,12 @@ export default class Section { return {}; } - let mapMarkers = []; + const mapMarkers = []; let centerCoordinates = {}; for (let j = 0; j < results.length; j++) { - let result = results[j]._raw; + const result = results[j]._raw; if (result && result.yextDisplayCoordinate) { if (!centerCoordinates.latitude) { centerCoordinates = { @@ -44,8 +44,8 @@ export default class Section { } return { - 'mapCenter': centerCoordinates, - 'mapMarkers': mapMarkers + mapCenter: centerCoordinates, + mapMarkers: mapMarkers }; } } diff --git a/src/core/models/verticalresults.js b/src/core/models/verticalresults.js index 0aed40750..a96c448da 100644 --- a/src/core/models/verticalresults.js +++ b/src/core/models/verticalresults.js @@ -46,7 +46,7 @@ export default class VerticalResults { * @param {string} verticalKeyFromRequest * @returns {@link Section} */ - static fromCore (verticalResults, urls = {}, formatters, resultsContext = ResultsContext.NORMAL, verticalKeyFromRequest) { + static fromCore (verticalResults, urls = {}, formatters = undefined, resultsContext = ResultsContext.NORMAL, verticalKeyFromRequest = undefined) { if (!verticalResults) { return new Section(); } diff --git a/src/core/search/searchdatatransformer.js b/src/core/search/searchdatatransformer.js index e51dc4974..e36310621 100644 --- a/src/core/search/searchdatatransformer.js +++ b/src/core/search/searchdatatransformer.js @@ -17,7 +17,7 @@ import ResultsContext from '../storage/resultscontext'; * component library and core storage understand. */ export default class SearchDataTransformer { - static transformUniversal (data, urls = {}, formatters) { + static transformUniversal (data, urls = {}, formatters = undefined) { return { [StorageKeys.QUERY_ID]: data.queryId, [StorageKeys.NAVIGATION]: Navigation.fromCore(data.verticalResults), diff --git a/src/core/services/errorreporterservice.js b/src/core/services/errorreporterservice.js index 033e10b1f..5186fd864 100644 --- a/src/core/services/errorreporterservice.js +++ b/src/core/services/errorreporterservice.js @@ -11,5 +11,5 @@ export default class ErrorReporterService { * Reports an error to backend servers for logging * @param {AnswersBaseError} err The error to be reported */ - report (err) {} // eslint-disable-line handle-callback-err + report (err) {} // eslint-disable-line } diff --git a/src/core/utils/arrayutils.js b/src/core/utils/arrayutils.js index d45ec77b7..1768906e4 100644 --- a/src/core/utils/arrayutils.js +++ b/src/core/utils/arrayutils.js @@ -16,7 +16,7 @@ export function groupArray (arr, keyFunc, valueFunc, initial) { const key = keyFunc(element, idx); const value = valueFunc(element, idx); if (!groups[key]) { - groups[key] = [ value ]; + groups[key] = [value]; } else { groups[key].push(value); } diff --git a/src/core/utils/configutils.js b/src/core/utils/configutils.js index fadd3b0fe..58a90de89 100644 --- a/src/core/utils/configutils.js +++ b/src/core/utils/configutils.js @@ -12,11 +12,11 @@ * @param {any} defaultValue */ export function defaultConfigOption (config, synonyms, defaultValue) { - for (let name of synonyms) { + for (const name of synonyms) { const accessors = name.split('.'); let parentConfig = config; let skip = false; - for (let childConfigAccessor of accessors.slice(0, -1)) { + for (const childConfigAccessor of accessors.slice(0, -1)) { if (!(childConfigAccessor in parentConfig)) { skip = true; break; diff --git a/src/core/utils/masterswitchapi.js b/src/core/utils/masterswitchapi.js deleted file mode 100644 index 9891e7e99..000000000 --- a/src/core/utils/masterswitchapi.js +++ /dev/null @@ -1,53 +0,0 @@ -import ApiRequest from '../http/apirequest'; - -/** - * This class provides access to the Answers Status page. This page indicates - * if the front-end for a particular experience should be temporarily disabled - * due to back-end issues. - */ -export default class MasterSwitchApi { - constructor (requestConfig, storage) { - this._request = new ApiRequest(requestConfig, storage); - } - - /** - * Checks if the front-end for the given experience should be temporarily disabled. - * Note that this check errs on the side of enabling the front-end. If the network call - * does not complete successfully, due to timeout or other error, those failures are caught. - * In these failure cases, the assumption is that things are enabled. - * - * @returns {Promise} A Promise containing a boolean indicating if the front-end - * should be disabled. - */ - isDisabled () { - // A 100ms timeout is enforced on the status call. - const timeout = new Promise((resolve, reject) => { - setTimeout(reject, 100); - }); - - return new Promise((resolve, reject) => { - Promise.race([this._request.get({ credentials: 'omit' }), timeout]) - .then(response => response.json()) - .then(status => status && status.disabled) - .then(isDisabled => resolve(!!isDisabled)) - .catch(() => resolve(false)); - }); - } - - /** - * Creates a new {@link MasterSwitchApi} from the provided parameters. - * - * @param {string} apiKey The apiKey of the experience. - * @param {string} experienceKey The identifier of the experience. - * @param {Storage} storage The {@link Storage} instance. - * @returns {MasterSwitchApi} The new {@link MasterSwitchApi} instance. - */ - static from (apiKey, experienceKey, storage) { - const requestConfig = { - apiKey, - baseUrl: 'https://answersstatus.pagescdn.com/', - endpoint: `${apiKey}/${experienceKey}/status.json` - }; - return new MasterSwitchApi(requestConfig, storage); - } -} diff --git a/src/ui/components/cards/accordioncardcomponent.js b/src/ui/components/cards/accordioncardcomponent.js index 228d68c10..bbaf7274a 100644 --- a/src/ui/components/cards/accordioncardcomponent.js +++ b/src/ui/components/cards/accordioncardcomponent.js @@ -1,3 +1,4 @@ +/* eslint-disable no-self-assign */ /** @module AccordionCardComponent */ import Component from '../component'; diff --git a/src/ui/components/cards/consts.js b/src/ui/components/cards/consts.js index 7e275f7a0..a25833faa 100644 --- a/src/ui/components/cards/consts.js +++ b/src/ui/components/cards/consts.js @@ -1,11 +1,11 @@ export const cardTemplates = { - 'Standard': 'cards/standard', - 'Accordion': 'cards/accordion', - 'Legacy': 'cards/legacy' + Standard: 'cards/standard', + Accordion: 'cards/accordion', + Legacy: 'cards/legacy' }; export const cardTypes = { - 'Standard': 'StandardCard', - 'Accordion': 'AccordionCard', - 'Legacy': 'LegacyCard' + Standard: 'StandardCard', + Accordion: 'AccordionCard', + Legacy: 'LegacyCard' }; diff --git a/src/ui/components/cards/legacycardcomponent.js b/src/ui/components/cards/legacycardcomponent.js index eefd5e333..3e7b5d655 100644 --- a/src/ui/components/cards/legacycardcomponent.js +++ b/src/ui/components/cards/legacycardcomponent.js @@ -1,3 +1,4 @@ +/* eslint-disable no-self-assign */ /** @module LegacyCardComponent */ import Component from '../component'; diff --git a/src/ui/components/cards/standardcardcomponent.js b/src/ui/components/cards/standardcardcomponent.js index b9b57f886..74d8175e2 100644 --- a/src/ui/components/cards/standardcardcomponent.js +++ b/src/ui/components/cards/standardcardcomponent.js @@ -1,3 +1,4 @@ +/* eslint-disable no-self-assign */ /** @module StandardCardComponent */ import Component from '../component'; diff --git a/src/ui/components/component.js b/src/ui/components/component.js index f70f101c1..69e73c84f 100644 --- a/src/ui/components/component.js +++ b/src/ui/components/component.js @@ -89,6 +89,12 @@ export default class Component { */ this._analyticsOptions = config.analyticsOptions || {}; + /** + * Allows the main thread to regain control while rendering child components + * @type {boolean} + */ + this._progressivelyRenderChildren = config.progressivelyRenderChildren; + /** * A reference to the DOM node that the component will be appended to when mounted/rendered. * @type {HTMLElement} @@ -158,7 +164,7 @@ export default class Component { * By default, no transformation happens. * @type {function} */ - this.transformData = config.transformData || this.transformData || function () {}; + this.transformData = config.transformData; /** * The a local reference to the callback that will be invoked when a component is created. @@ -278,12 +284,8 @@ export default class Component { return this._state.has(prop); } - transformData (data) { - return data; - } - addChild (data, type, opts) { - let childComponent = this.componentManager.create( + const childComponent = this.componentManager.create( type, Object.assign({ name: data.name, @@ -367,16 +369,26 @@ export default class Component { // Process the DOM to determine if we should create // in-memory sub-components for rendering const domComponents = DOM.queryAll(this._container, '[data-component]:not([data-is-component-mounted])'); - const data = this.transformData(cloneDeep(this._state.get())); + const data = this.transformData + ? this.transformData(cloneDeep(this._state.get())) + : this._state.get(); domComponents.forEach(c => this._createSubcomponent(c, data)); - this._children.forEach(child => { - child.mount(); - }); + if (this._progressivelyRenderChildren) { + this._children.forEach(child => { + setTimeout(() => { + child.mount(); + }); + }); + } else { + this._children.forEach(child => { + child.mount(); + }); + } // Attach analytics hooks as necessary if (this.analyticsReporter) { - let domHooks = DOM.queryAll(this._container, '[data-eventtype]:not([data-is-analytics-attached])'); + const domHooks = DOM.queryAll(this._container, '[data-eventtype]:not([data-is-analytics-attached])'); domHooks.forEach(this._createAnalyticsHook.bind(this)); } @@ -384,6 +396,8 @@ export default class Component { this.onMount(this); this.userOnMount(this); + DOM.removeClass(this._container, 'yxt-Answers-component--unmounted'); + return this; } @@ -394,7 +408,9 @@ export default class Component { render (data = this._state.get()) { this.beforeRender(); // Temporary fix for passing immutable data to transformData(). - data = this.transformData(cloneDeep(data)); + data = this.transformData + ? this.transformData(cloneDeep(data)) + : data; let html = ''; // Use either the custom render function or the internal renderer @@ -412,12 +428,8 @@ export default class Component { }, data); } - // We create an HTML Document fragment with the rendered string - // So that we can query it for processing of sub components - let el = DOM.create(html); - this.afterRender(); - return el.innerHTML; + return html; } _createSubcomponent (domComponent, data) { @@ -427,7 +439,7 @@ export default class Component { const prop = dataset.prop; let opts = dataset.opts ? JSON.parse(dataset.opts) : {}; - let childData = data[prop] || {}; + const childData = data[prop] || {}; opts = { ...opts, diff --git a/src/ui/components/componentmanager.js b/src/ui/components/componentmanager.js index f56b18660..d2bf71d1a 100644 --- a/src/ui/components/componentmanager.js +++ b/src/ui/components/componentmanager.js @@ -117,7 +117,7 @@ export default class ComponentManager { // Every component needs local access to the component manager // because sometimes components have subcomponents that need to be // constructed during creation - let systemOpts = { + const systemOpts = { core: this._core, renderer: this._renderer, analyticsReporter: this._analyticsReporter, @@ -126,7 +126,7 @@ export default class ComponentManager { }; this._componentIdCounter++; - let componentClass = COMPONENT_REGISTRY[componentType]; + const componentClass = COMPONENT_REGISTRY[componentType]; if (!componentClass) { throw new AnswersComponentError( `Component type ${componentType} is not recognized as a valid component.` + @@ -148,7 +148,7 @@ export default class ComponentManager { }; // Instantiate our new component and keep track of it - let component = + const component = new COMPONENT_REGISTRY[componentType](config, systemOpts) .init(config); diff --git a/src/ui/components/ctas/ctacollectioncomponent.js b/src/ui/components/ctas/ctacollectioncomponent.js index d708f7561..d9927efbe 100644 --- a/src/ui/components/ctas/ctacollectioncomponent.js +++ b/src/ui/components/ctas/ctacollectioncomponent.js @@ -71,12 +71,12 @@ export default class CTACollectionComponent extends Component { */ static resolveCTAMapping (result, ...ctas) { let parsedCTAs = []; - ctas.map(ctaMapping => { + ctas.forEach(ctaMapping => { if (typeof ctaMapping === 'function') { parsedCTAs = parsedCTAs.concat(ctaMapping(result)); } else if (typeof ctaMapping === 'object') { const ctaObject = { ...ctaMapping }; - for (let [ctaAttribute, attributeMapping] of Object.entries(ctaMapping)) { + for (const [ctaAttribute, attributeMapping] of Object.entries(ctaMapping)) { if (typeof attributeMapping === 'function') { ctaObject[ctaAttribute] = attributeMapping(result); } diff --git a/src/ui/components/ctas/ctacomponent.js b/src/ui/components/ctas/ctacomponent.js index fedeb2a40..2b6e50bf9 100644 --- a/src/ui/components/ctas/ctacomponent.js +++ b/src/ui/components/ctas/ctacomponent.js @@ -1,3 +1,4 @@ +/* eslint-disable no-self-assign */ /** @module CTAComponent */ import Component from '../component'; @@ -80,7 +81,7 @@ export default class CTAComponent extends Component { } onMount () { - const el = DOM.query(this._container, `.js-yxt-CTA`); + const el = DOM.query(this._container, '.js-yxt-CTA'); if (el && this._config.eventOptions) { DOM.on(el, 'mousedown', e => { if (e.button === 0 || e.button === 1) { diff --git a/src/ui/components/filters/facetscomponent.js b/src/ui/components/filters/facetscomponent.js index 5c5551622..a5086cd56 100644 --- a/src/ui/components/filters/facetscomponent.js +++ b/src/ui/components/filters/facetscomponent.js @@ -234,7 +234,7 @@ export default class FacetsComponent extends Component { } setState (data) { - let facets = data['filters'] || []; + let facets = data.filters || []; if (this._transformFacets) { const facetsCopy = cloneDeep(facets); @@ -266,7 +266,7 @@ export default class FacetsComponent extends Component { // passed to FilterOptionsConfig. Even after deletion here, the filter options will still // exist in the 'filters' field of the facets component state, and therefore any // modifications which occur to options inside transformFacets will still take effect. - filterOptions['options'] && delete filterOptions['options']; + filterOptions.options && delete filterOptions.options; acc[currentFacet.fieldId] = filterOptions; return acc; }, {}); @@ -281,7 +281,7 @@ export default class FacetsComponent extends Component { _applyDefaultFormatting (facet) { const isBooleanFacet = facet => { const firstOption = (facet.options && facet.options[0]) || {}; - return firstOption['value'] === true || firstOption['value'] === false; + return firstOption.value === true || firstOption.value === false; }; if (isBooleanFacet(facet)) { diff --git a/src/ui/components/filters/filterboxcomponent.js b/src/ui/components/filters/filterboxcomponent.js index 543aea4e5..b8a00b484 100644 --- a/src/ui/components/filters/filterboxcomponent.js +++ b/src/ui/components/filters/filterboxcomponent.js @@ -176,7 +176,7 @@ export default class FilterBoxComponent extends Component { this._filterNodes = []; this.config.filterConfigs.forEach(config => { - let hideCount = config.showCount === undefined ? !this.config.showCount : !config.showCount; + const hideCount = config.showCount === undefined ? !this.config.showCount : !config.showCount; if (hideCount) { config.options.forEach(option => { @@ -252,7 +252,7 @@ export default class FilterBoxComponent extends Component { } // Initialize reset button - let resetEl = DOM.query(this._container, '.js-yxt-FilterBox-reset'); + const resetEl = DOM.query(this._container, '.js-yxt-FilterBox-reset'); if (resetEl) { DOM.on(resetEl, 'click', this.resetFilters.bind(this)); diff --git a/src/ui/components/filters/filteroptionscomponent.js b/src/ui/components/filters/filteroptionscomponent.js index 0b366b2a0..c68206b06 100644 --- a/src/ui/components/filters/filteroptionscomponent.js +++ b/src/ui/components/filters/filteroptionscomponent.js @@ -347,7 +347,7 @@ export default class FilterOptionsComponent extends Component { * @override */ static defaultTemplateName (config) { - return `controls/filteroptions`; + return 'controls/filteroptions'; } setState (data) { @@ -365,11 +365,11 @@ export default class FilterOptionsComponent extends Component { onMount () { DOM.delegate( - DOM.query(this._container, `.yxt-FilterOptions-options`), + DOM.query(this._container, '.yxt-FilterOptions-options'), this.config.optionSelector, 'click', event => { - let selectedCountEl = DOM.query(this._container, '.js-yxt-FilterOptions-selectedCount'); + const selectedCountEl = DOM.query(this._container, '.js-yxt-FilterOptions-selectedCount'); if (selectedCountEl) { selectedCountEl.innerText = this._getSelectedCount(); } @@ -394,7 +394,7 @@ export default class FilterOptionsComponent extends Component { this.showMoreState = true; showLessEl.classList.add('hidden'); showMoreEl.classList.remove('hidden'); - for (let optionEl of optionsOverLimitEls) { + for (const optionEl of optionsOverLimitEls) { optionEl.classList.add('hidden'); } }); @@ -405,7 +405,7 @@ export default class FilterOptionsComponent extends Component { this.showMoreState = false; showLessEl.classList.remove('hidden'); showMoreEl.classList.add('hidden'); - for (let optionEl of optionsOverLimitEls) { + for (const optionEl of optionsOverLimitEls) { optionEl.classList.remove('hidden'); } }); @@ -441,7 +441,7 @@ export default class FilterOptionsComponent extends Component { clearSearchEl.classList.remove('js-hidden'); } - for (let filterOption of filterOptionEls) { + for (const filterOption of filterOptionEls) { const labelEl = DOM.query(filterOption, '.js-yxt-FilterOptions-optionLabel--name'); let labelText = labelEl.textContent || labelEl.innerText || ''; labelText = labelText.trim(); @@ -450,7 +450,7 @@ export default class FilterOptionsComponent extends Component { filterOption.classList.remove('displaySearch'); labelEl.innerHTML = labelText; } else { - let matchedSubstring = this._getMatchedSubstring(labelText.toLowerCase(), filter.toLowerCase()); + const matchedSubstring = this._getMatchedSubstring(labelText.toLowerCase(), filter.toLowerCase()); if (matchedSubstring) { filterOption.classList.add('displaySearch'); filterOption.classList.remove('hiddenSearch'); @@ -550,7 +550,7 @@ export default class FilterOptionsComponent extends Component { const maxLevenshteinDistance = 1; if (filter.length > minFilterSizeForLevenshtein) { // Break option into X filter.length size substrings - let substrings = []; + const substrings = []; for (let start = 0; start <= (option.length - filter.length); start++) { substrings.push(option.substr(start, filter.length)); } @@ -558,8 +558,8 @@ export default class FilterOptionsComponent extends Component { // Find the substring that is the closest in levenshtein distance to filter let minLevDist = filter.length; let minLevSubstring = filter; - for (let substring of substrings) { - let levDist = this._calcLevenshteinDistance(substring, filter); + for (const substring of substrings) { + const levDist = this._calcLevenshteinDistance(substring, filter); if (levDist < minLevDist) { minLevDist = levDist; minLevSubstring = substring; diff --git a/src/ui/components/map/mapcomponent.js b/src/ui/components/map/mapcomponent.js index 9f968e410..b3866db6c 100644 --- a/src/ui/components/map/mapcomponent.js +++ b/src/ui/components/map/mapcomponent.js @@ -9,8 +9,8 @@ import StorageKeys from '../../../core/storage/storagekeys'; import ResultsContext from '../../../core/storage/resultscontext'; const ProviderTypes = { - 'google': GoogleMapProvider, - 'mapbox': MapBoxMapProvider + google: GoogleMapProvider, + mapbox: MapBoxMapProvider }; export default class MapComponent extends Component { diff --git a/src/ui/components/map/providers/googlemapprovider.js b/src/ui/components/map/providers/googlemapprovider.js index 101aad974..4c2f23ae6 100644 --- a/src/ui/components/map/providers/googlemapprovider.js +++ b/src/ui/components/map/providers/googlemapprovider.js @@ -108,7 +108,7 @@ export default class GoogleMapProvider extends MapProvider { // NOTE(billy) This timeout is a hack for dealing with async nature. // Only here for demo purposes, so we'll fix later. setTimeout(() => { - let container = DOM.query(el); + const container = DOM.query(el); this.map = new google.maps.Map(container, { zoom: this._zoom, center: this.getCenterMarker(mapData) @@ -119,14 +119,14 @@ export default class GoogleMapProvider extends MapProvider { const collapsedMarkers = this._collapsePins ? this._collapseMarkers(mapData.mapMarkers) : mapData.mapMarkers; - let googleMapMarkerConfigs = GoogleMapMarkerConfig.from( + const googleMapMarkerConfigs = GoogleMapMarkerConfig.from( collapsedMarkers, this._pinConfig, this.map); - let bounds = new google.maps.LatLngBounds(); + const bounds = new google.maps.LatLngBounds(); for (let i = 0; i < googleMapMarkerConfigs.length; i++) { - let marker = new google.maps.Marker(googleMapMarkerConfigs[i]); + const marker = new google.maps.Marker(googleMapMarkerConfigs[i]); if (this._onPinClick) { marker.addListener('click', () => this._onPinClick(collapsedMarkers[i].item)); } @@ -196,7 +196,7 @@ export class GoogleMapMarkerConfig { * @returns {string[]} */ static serialize (googleMapMarkerConfigs) { - let serializedMarkers = []; + const serializedMarkers = []; googleMapMarkerConfigs.forEach((marker) => { serializedMarkers.push(`markers=label:${marker.label}|${marker.position.lat},${marker.position.lng}`); }); @@ -211,7 +211,7 @@ export class GoogleMapMarkerConfig { * @returns {GoogleMapMarkerConfig[]} */ static from (markers, pinConfig, map) { - let googleMapMarkerConfigs = []; + const googleMapMarkerConfigs = []; if (!Array.isArray(markers)) { markers = [markers]; } diff --git a/src/ui/components/map/providers/mapboxmapprovider.js b/src/ui/components/map/providers/mapboxmapprovider.js index f89cc8759..adb11a7f0 100644 --- a/src/ui/components/map/providers/mapboxmapprovider.js +++ b/src/ui/components/map/providers/mapboxmapprovider.js @@ -28,7 +28,7 @@ export default class MapBoxMapProvider extends MapProvider { * @param {function} onLoad An optional callback to invoke once the JS is loaded. */ loadJS (onLoad) { - let script = DOM.createEl('script', { + const script = DOM.createEl('script', { id: 'yext-map-js', onload: () => { this._isLoaded = true; @@ -46,7 +46,7 @@ export default class MapBoxMapProvider extends MapProvider { src: 'https://api.mapbox.com/mapbox-gl-js/v0.44.1/mapbox-gl.js' }); - let css = DOM.createEl('link', { + const css = DOM.createEl('link', { id: 'yext-map-css', rel: 'stylesheet', href: 'https://api.mapbox.com/mapbox-gl-js/v0.44.1/mapbox-gl.css' @@ -62,7 +62,7 @@ export default class MapBoxMapProvider extends MapProvider { return this; } - let container = DOM.query(el); + const container = DOM.query(el); this._map = new mapboxgl.Map({ container: container, zoom: this._zoom, @@ -85,11 +85,11 @@ export default class MapBoxMapProvider extends MapProvider { const bounds = new mapboxgl.LngLatBounds(); for (let i = 0; i < mapboxMapMarkerConfigs.length; i++) { - let wrapper = mapboxMapMarkerConfigs[i].wrapper; - let coords = new mapboxgl.LngLat( + const wrapper = mapboxMapMarkerConfigs[i].wrapper; + const coords = new mapboxgl.LngLat( mapboxMapMarkerConfigs[i].position.longitude, mapboxMapMarkerConfigs[i].position.latitude); - let marker = new mapboxgl.Marker(wrapper).setLngLat(coords); + const marker = new mapboxgl.Marker(wrapper).setLngLat(coords); bounds.extend(marker.getLngLat()); marker.addTo(this._map); if (this._onPinClick) { @@ -159,7 +159,7 @@ export class MapBoxMarkerConfig { * @returns {string[]} */ static serialize (mapboxMapMarkerConfigs) { - let serializedMarkers = []; + const serializedMarkers = []; mapboxMapMarkerConfigs.forEach((marker) => { if (marker.staticMapPin) { serializedMarkers.push(`url-${marker.staticMapPin}(${marker.position.longitude},${marker.position.latitude})`); @@ -178,7 +178,7 @@ export class MapBoxMarkerConfig { * @returns {MapBoxMarkerConfig[]} */ static from (markers, pinConfig, map) { - let mapboxMapMarkerConfigs = []; + const mapboxMapMarkerConfigs = []; if (!Array.isArray(markers)) { markers = [markers]; } diff --git a/src/ui/components/map/providers/mapprovider.js b/src/ui/components/map/providers/mapprovider.js index 03bf4d559..89c2d857a 100644 --- a/src/ui/components/map/providers/mapprovider.js +++ b/src/ui/components/map/providers/mapprovider.js @@ -173,7 +173,7 @@ export default class MapProvider { }); const collapsedMarkers = []; - for (let [, markers] of Object.entries(locationToItem)) { + for (const [, markers] of Object.entries(locationToItem)) { if (markers.length > 1) { const collapsedMarker = { item: markers.map(m => m.item), diff --git a/src/ui/components/navigation/navigationcomponent.js b/src/ui/components/navigation/navigationcomponent.js index f4ca84ae6..73babcfb1 100644 --- a/src/ui/components/navigation/navigationcomponent.js +++ b/src/ui/components/navigation/navigationcomponent.js @@ -392,7 +392,7 @@ export default class NavigationComponent extends Component { // ParentNode.prepend polyfill // https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/prepend#Polyfill _prepend (collapsedLinks, lastLink) { - if (!collapsedLinks.hasOwnProperty('prepend')) { + if (!collapsedLinks.hasOwnProperty('prepend')) { // eslint-disable-line no-prototype-builtins const docFrag = document.createDocumentFragment(); const isNode = lastLink instanceof Node; docFrag.appendChild(isNode ? lastLink : document.createTextNode(String(lastLink))); @@ -408,7 +408,7 @@ export default class NavigationComponent extends Component { // Adapted from Element.closest polyfill // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill _closest (el, closestElSelector) { - if (!el.hasOwnProperty('closest')) { + if (!el.hasOwnProperty('closest')) { // eslint-disable-line no-prototype-builtins do { if (DOM.matches(el, closestElSelector)) return el; el = el.parentElement || el.parentNode; @@ -422,10 +422,11 @@ export default class NavigationComponent extends Component { switch (this._mobileOverflowBehavior) { case MOBILE_OVERFLOW_BEHAVIOR_OPTION.COLLAPSE: return true; - case MOBILE_OVERFLOW_BEHAVIOR_OPTION.INNERSCROLL: + case MOBILE_OVERFLOW_BEHAVIOR_OPTION.INNERSCROLL: { const container = DOM.query(this._container, '.yxt-Nav-container') || this._container; const navWidth = container.offsetWidth; return navWidth > MOBILE_BREAKPOINT; + } } } } diff --git a/src/ui/components/questions/questionsubmissioncomponent.js b/src/ui/components/questions/questionsubmissioncomponent.js index cad8e93dc..ba1d5e2fa 100644 --- a/src/ui/components/questions/questionsubmissioncomponent.js +++ b/src/ui/components/questions/questionsubmissioncomponent.js @@ -19,19 +19,19 @@ const DEFAULT_CONFIG = { * This is typically an organization object * @type {number} */ - 'entityId': null, + entityId: null, /** * The main CSS selector used to reference the form for the component. * @type {string} CSS selector */ - 'formSelector': 'form', + formSelector: 'form', /** * An optional label to use for the e-mail address input * @type {string} */ - 'emailLabel': TranslationFlagger.flag({ + emailLabel: TranslationFlagger.flag({ phrase: 'Email', context: 'Labels the email value provided as an argument' }), @@ -40,7 +40,7 @@ const DEFAULT_CONFIG = { * An optional label to use for the name input * @type {string} */ - 'nameLabel': TranslationFlagger.flag({ + nameLabel: TranslationFlagger.flag({ phrase: 'Name', context: 'Labels the name value provided as an argument' }), @@ -49,7 +49,7 @@ const DEFAULT_CONFIG = { * An optional label to use for the question * @type {string} */ - 'questionLabel': TranslationFlagger.flag({ + questionLabel: TranslationFlagger.flag({ phrase: 'Question', context: 'Labels the question value provided as an argument' }), @@ -58,7 +58,7 @@ const DEFAULT_CONFIG = { * An optional label to use for the Privacy Policy * @type {string} */ - 'privacyPolicyText': TranslationFlagger.flag({ + privacyPolicyText: TranslationFlagger.flag({ phrase: 'By submitting my email address, I consent to being contacted via email at the address provided.' }), @@ -66,7 +66,7 @@ const DEFAULT_CONFIG = { * The label to use for the Submit button * @type {string} */ - 'buttonLabel': TranslationFlagger.flag({ + buttonLabel: TranslationFlagger.flag({ phrase: 'Submit', context: 'Button label' }), @@ -75,7 +75,7 @@ const DEFAULT_CONFIG = { * The title to display in the title bar * @type {string} */ - 'sectionTitle': TranslationFlagger.flag({ + sectionTitle: TranslationFlagger.flag({ phrase: 'Ask a Question', context: 'Title of section' }), @@ -84,7 +84,7 @@ const DEFAULT_CONFIG = { * The description to display in the title bar * @type {string} */ - 'teaser': TranslationFlagger.flag({ + teaser: TranslationFlagger.flag({ phrase: 'Can’t find what you\'re looking for? Ask a question below.' }), @@ -92,13 +92,13 @@ const DEFAULT_CONFIG = { * The name of the icon to use in the title bar * @type {string} */ - 'sectionTitleIconName': 'support', + sectionTitleIconName: 'support', /** * The text to display in the feedback form ahead of the Question input * @type {string} */ - 'description': TranslationFlagger.flag({ + description: TranslationFlagger.flag({ phrase: 'Enter your question and contact information, and we\'ll get back to you with a response shortly.' }), @@ -106,7 +106,7 @@ const DEFAULT_CONFIG = { * The placeholder text for required inputs * @type {string} */ - 'requiredInputPlaceholder': TranslationFlagger.flag({ + requiredInputPlaceholder: TranslationFlagger.flag({ phrase: '(required)', context: 'Indicates that entering input is mandatory' }), @@ -115,7 +115,7 @@ const DEFAULT_CONFIG = { * The placeholder text for the question text area * @type {string} */ - 'questionInputPlaceholder': TranslationFlagger.flag({ + questionInputPlaceholder: TranslationFlagger.flag({ phrase: 'Enter your question here', context: 'Placeholder text for input field' }), @@ -124,7 +124,7 @@ const DEFAULT_CONFIG = { * The confirmation text to display after successfully submitting feedback * @type {string} */ - 'questionSubmissionConfirmationText': TranslationFlagger.flag({ + questionSubmissionConfirmationText: TranslationFlagger.flag({ phrase: 'Thank you for your question!' }), @@ -132,7 +132,7 @@ const DEFAULT_CONFIG = { * The default privacy policy url label * @type {string} */ - 'privacyPolicyUrlLabel': TranslationFlagger.flag({ + privacyPolicyUrlLabel: TranslationFlagger.flag({ phrase: 'Learn more here.', context: 'Labels a link' }), @@ -141,13 +141,13 @@ const DEFAULT_CONFIG = { * The default privacy policy url * @type {string} */ - 'privacyPolicyUrl': '', + privacyPolicyUrl: '', /** * The default privacy policy error text, shown when the user does not agree * @type {string} */ - 'privacyPolicyErrorText': TranslationFlagger.flag({ + privacyPolicyErrorText: TranslationFlagger.flag({ phrase: '* You must agree to the privacy policy to submit a question.' }), @@ -155,7 +155,7 @@ const DEFAULT_CONFIG = { * The default email format error text, shown when the user submits an invalid email * @type {string} */ - 'emailFormatErrorText': TranslationFlagger.flag({ + emailFormatErrorText: TranslationFlagger.flag({ phrase: '* Please enter a valid email address.' }), @@ -164,7 +164,7 @@ const DEFAULT_CONFIG = { * request. * @type {string} */ - 'networkErrorText': TranslationFlagger.flag({ + networkErrorText: TranslationFlagger.flag({ phrase: 'We\'re sorry, an error occurred.' }), @@ -172,7 +172,7 @@ const DEFAULT_CONFIG = { * Whether or not this component is expanded by default. * @type {boolean} */ - 'expanded': true + expanded: true }; /** @@ -268,12 +268,12 @@ export default class QuestionSubmissionComponent extends Component { } onMount () { - let triggerEl = DOM.query(this._container, '.js-content-visibility-toggle'); + const triggerEl = DOM.query(this._container, '.js-content-visibility-toggle'); if (triggerEl !== null) { this.bindFormToggle(triggerEl); } - let formEl = DOM.query(this._container, this._config.formSelector); + const formEl = DOM.query(this._container, this._config.formSelector); if (formEl === null) { return; } @@ -315,17 +315,17 @@ export default class QuestionSubmissionComponent extends Component { } this.core.submitQuestion({ - 'entityId': this._config.entityId, - 'site': 'FIRSTPARTY', - 'name': formData.name, - 'email': formData.email, - 'questionText': formData.questionText, - 'questionDescription': formData.questionDescription + entityId: this._config.entityId, + site: 'FIRSTPARTY', + name: formData.name, + email: formData.email, + questionText: formData.questionText, + questionDescription: formData.questionDescription }) .catch(error => { this.setState( new QuestionSubmission(formData, { - 'network': 'We\'re sorry, an error occurred.' + network: 'We\'re sorry, an error occurred.' }) ); throw error; @@ -343,8 +343,9 @@ export default class QuestionSubmissionComponent extends Component { this.setState( new QuestionSubmission({ ...formData, - 'expanded': !formData.questionExpanded, - 'submitted': formData.questionSubmitted }, + expanded: !formData.questionExpanded, + submitted: formData.questionSubmitted + }, formData.errors)); }); } @@ -361,7 +362,7 @@ export default class QuestionSubmissionComponent extends Component { return {}; } - let obj = {}; + const obj = {}; for (let i = 0; i < inputFields.length; i++) { let val = inputFields[i].value; if (inputFields[i].type === 'checkbox') { @@ -379,7 +380,7 @@ export default class QuestionSubmissionComponent extends Component { * @returns {Object} errors object if any errors found */ validate (formEl) { - let errors = {}; + const errors = {}; const fields = DOM.queryAll(formEl, '.js-question-field'); for (let i = 0; i < fields.length; i++) { if (!fields[i].checkValidity()) { @@ -389,20 +390,20 @@ export default class QuestionSubmissionComponent extends Component { } switch (fields[i].name) { case 'email': - errors['emailError'] = true; + errors.emailError = true; if (!fields[i].validity.valueMissing) { - errors['emailErrorText'] = this._config.emailFormatErrorText; + errors.emailErrorText = this._config.emailFormatErrorText; } break; case 'name': - errors['nameError'] = true; + errors.nameError = true; break; case 'privacyPolicy': - errors['privacyPolicyErrorText'] = this._config.privacyPolicyErrorText; - errors['privacyPolicyError'] = true; + errors.privacyPolicyErrorText = this._config.privacyPolicyErrorText; + errors.privacyPolicyError = true; break; case 'questionText': - errors['questionTextError'] = true; + errors.questionTextError = true; break; } } diff --git a/src/ui/components/results/alternativeverticalscomponent.js b/src/ui/components/results/alternativeverticalscomponent.js index 1ff661bbd..c231de4dd 100644 --- a/src/ui/components/results/alternativeverticalscomponent.js +++ b/src/ui/components/results/alternativeverticalscomponent.js @@ -135,7 +135,7 @@ export default class AlternativeVerticalsComponent extends Component { * @param {object} verticalsConfig the configuration to use */ _buildVerticalSuggestions (alternativeVerticals, verticalsConfig, context, referrerPageUrl) { - let verticals = []; + const verticals = []; const params = new SearchParams(this.core.storage.getCurrentStateUrlMerged()); if (context) { diff --git a/src/ui/components/results/directanswercomponent.js b/src/ui/components/results/directanswercomponent.js index fb02b5a1b..b1374ce2c 100644 --- a/src/ui/components/results/directanswercomponent.js +++ b/src/ui/components/results/directanswercomponent.js @@ -151,7 +151,7 @@ export default class DirectAnswerComponent extends Component { this.reportQuality(checkedValue); this.updateState({ - 'feedbackSubmitted': true + feedbackSubmitted: true }); }); @@ -299,7 +299,7 @@ export default class DirectAnswerComponent extends Component { fieldName: directAnswer.answer.fieldName, fieldType: directAnswer.answer.fieldType }; - for (let [propertyToMatch, propertyValue] of Object.entries(override)) { + for (const [propertyToMatch, propertyValue] of Object.entries(override)) { if (propertyToMatch === 'cardType') { continue; } @@ -349,7 +349,7 @@ export default class DirectAnswerComponent extends Component { const eventType = isGood === true ? EventTypes.THUMBS_UP : EventTypes.THUMBS_DOWN; const event = new AnalyticsEvent(eventType) .addOptions({ - 'directAnswer': true + directAnswer: true }); this.analyticsReporter.report(event); diff --git a/src/ui/components/results/paginationcomponent.js b/src/ui/components/results/paginationcomponent.js index 808e5b942..8e286e792 100644 --- a/src/ui/components/results/paginationcomponent.js +++ b/src/ui/components/results/paginationcomponent.js @@ -227,9 +227,9 @@ export default class PaginationComponent extends Component { * @returns {Array} the backLimit and frontLimit, respectively */ _allocate (pageNumber, maxPage, limit) { - var backLimit = pageNumber; - var frontLimit = pageNumber; - for (var i = 0; i < limit; i++) { + let backLimit = pageNumber; + let frontLimit = pageNumber; + for (let i = 0; i < limit; i++) { if (i % 2 === 0) { if (backLimit > 0) { backLimit--; @@ -258,7 +258,7 @@ export default class PaginationComponent extends Component { const [mobileBackLimit, mobileFrontLimit] = this._allocate(pageNumber, maxPage, this._maxVisiblePagesMobile); const [desktopBackLimit, desktopFrontLimit] = this._allocate(pageNumber, maxPage, this._maxVisiblePagesDesktop); const pageNumberViews = []; - for (var i = 1; i <= maxPage; i++) { + for (let i = 1; i <= maxPage; i++) { const num = { number: i }; if (i === pageNumber) { num.active = true; diff --git a/src/ui/components/results/universalresultscomponent.js b/src/ui/components/results/universalresultscomponent.js index 9bcc63982..82c387507 100644 --- a/src/ui/components/results/universalresultscomponent.js +++ b/src/ui/components/results/universalresultscomponent.js @@ -78,7 +78,7 @@ export default class UniversalResultsComponent extends Component { }, val)); } - addChild (data = {}, type, opts) { + addChild (data = {}, type = undefined, opts = undefined) { const verticals = this._config.verticals || this._config.config || {}; const verticalKey = data.verticalConfigId; const childOpts = { diff --git a/src/ui/components/results/verticalresultscomponent.js b/src/ui/components/results/verticalresultscomponent.js index 6ea722ccb..6089bc47d 100644 --- a/src/ui/components/results/verticalresultscomponent.js +++ b/src/ui/components/results/verticalresultscomponent.js @@ -348,7 +348,7 @@ export default class VerticalResultsComponent extends Component { this.addContainerClass(getContainerClass(searchState)); } - setState (data = {}, val) { + setState (data = {}, val = undefined) { /** * @type {Array} */ diff --git a/src/ui/components/search/autocompletecomponent.js b/src/ui/components/search/autocompletecomponent.js index abd349e9c..90e7be7cb 100644 --- a/src/ui/components/search/autocompletecomponent.js +++ b/src/ui/components/search/autocompletecomponent.js @@ -188,8 +188,8 @@ export default class AutoCompleteComponent extends Component { } isQueryInputFocused () { - return document.activeElement && - document.activeElement.className.includes(this._inputEl.substring(1)); + return document.activeElement && document.activeElement.getAttribute('class') && + document.activeElement.getAttribute('class').includes(this._inputEl.substring(1)); } /** @@ -205,7 +205,7 @@ export default class AutoCompleteComponent extends Component { */ onCreate () { // Use the context of the parent component to find the input node. - let queryInput = DOM.query(this._parentContainer, this._inputEl); + const queryInput = DOM.query(this._parentContainer, this._inputEl); if (!queryInput) { throw new Error('Could not initialize AutoComplete. Can not find {HTMLElement} `', this._inputEl, '`.'); } @@ -248,8 +248,8 @@ export default class AutoCompleteComponent extends Component { // Allow the user to select a result with the mouse DOM.delegate(this._container, '.js-yext-autocomplete-option', 'click', (evt, target) => { - let data = target.dataset; - let val = data.short; + const data = target.dataset; + const val = data.short; this.updateQuery(val); this._onSubmit(val, data.filter); @@ -290,18 +290,18 @@ export default class AutoCompleteComponent extends Component { // If one is provided, great. // Otherwise, lets try to find it from the current selection in the results. if (optValue === undefined) { - let sections = this._state.get('sections'); + const sections = this._state.get('sections'); - let results = sections[this._sectionIndex].results; + const results = sections[this._sectionIndex].results; optValue = results[this._resultIndex].shortValue; } - let queryEl = DOM.query(this._parentContainer, this._inputEl); + const queryEl = DOM.query(this._parentContainer, this._inputEl); queryEl.value = optValue; } handleTyping (key, value, e) { - let ignoredKeys = [ + const ignoredKeys = [ Keys.DOWN, Keys.UP, Keys.CTRL, @@ -356,7 +356,7 @@ export default class AutoCompleteComponent extends Component { if (!data) { return false; } - let sections = data['sections']; + const sections = data.sections; if (!sections) { return false; } @@ -380,7 +380,7 @@ export default class AutoCompleteComponent extends Component { } handleNavigateResults (key, e) { - let sections = this._state.get('sections'); + const sections = this._state.get('sections'); if (sections === undefined || sections.length <= 0) { return; } @@ -391,7 +391,7 @@ export default class AutoCompleteComponent extends Component { return; } - let results = sections[this._sectionIndex].results; + const results = sections[this._sectionIndex].results; if (key === Keys.UP) { e.preventDefault(); if (this._resultIndex <= 0) { @@ -433,7 +433,7 @@ export default class AutoCompleteComponent extends Component { } handleSubmitResult (key, value, e) { - let sections = this._state.get('sections'); + const sections = this._state.get('sections'); if (sections === undefined || sections.length <= 0) { if (this.isFilterSearch) { this.autoComplete(value); diff --git a/src/ui/components/search/searchcomponent.js b/src/ui/components/search/searchcomponent.js index e6aefe2da..bb75ee56a 100644 --- a/src/ui/components/search/searchcomponent.js +++ b/src/ui/components/search/searchcomponent.js @@ -9,8 +9,8 @@ import QueryUpdateListener from '../../../core/statelisteners/queryupdatelistene import QueryTriggers from '../../../core/models/querytriggers'; const IconState = { - 'YEXT': 0, - 'MAGNIFYING_GLASS': 1 + YEXT: 0, + MAGNIFYING_GLASS: 1 }; /** @@ -416,8 +416,11 @@ export default class SearchComponent extends Component { initClearButton () { const button = this._getClearButton(); this._showClearButton = this._showClearButton || this.query; - button.classList.toggle('yxt-SearchBar--hidden', !this._showClearButton); - + if (this._showClearButton) { + button.classList.remove('yxt-SearchBar--hidden'); + } else { + button.classList.add('yxt-SearchBar--hidden'); + } DOM.on(button, 'click', () => { this.customHooks.onClearSearch(); this.query = ''; @@ -456,7 +459,7 @@ export default class SearchComponent extends Component { this._container.classList.add('yxt-SearchBar-wrapper'); if (this._useForm) { - let form = DOM.query(this._container, formSelector); + const form = DOM.query(this._container, formSelector); if (!form) { throw new Error( 'Could not initialize SearchBar; Can not find {HTMLElement} `', @@ -523,17 +526,6 @@ export default class SearchComponent extends Component { inputEl.blur(); DOM.query(this._container, '.js-yext-submit').blur(); - // TODO: move this into initClearButton - if (this.clearButton) { - const button = DOM.query(this._container, '.js-yxt-SearchBar-clear'); - if (this.query) { - this._showClearButton = true; - button.classList.remove('yxt-SearchBar--hidden'); - } else { - this._showClearButton = false; - button.classList.add('yxt-SearchBar--hidden'); - } - } if (this.isUsingYextAnimatedIcon) { this.animateIconToYext(); } diff --git a/src/ui/components/search/spellcheckcomponent.js b/src/ui/components/search/spellcheckcomponent.js index 08d55810a..1a2f31540 100644 --- a/src/ui/components/search/spellcheckcomponent.js +++ b/src/ui/components/search/spellcheckcomponent.js @@ -45,7 +45,7 @@ export default class SpellCheckComponent extends Component { if (query === undefined) { return ''; } - let params = new SearchParams(this.core.storage.getCurrentStateUrlMerged()); + const params = new SearchParams(this.core.storage.getCurrentStateUrlMerged()); params.set(StorageKeys.QUERY, query.value); params.set(StorageKeys.SKIP_SPELL_CHECK, true); params.set(StorageKeys.QUERY_TRIGGER, type.toLowerCase()); diff --git a/src/ui/dom/dom.js b/src/ui/dom/dom.js index 0ffcc50e2..a2e614368 100644 --- a/src/ui/dom/dom.js +++ b/src/ui/dom/dom.js @@ -98,8 +98,8 @@ export default class DOM { * @param {Object} opts_data Optional attributes to apply to the new HTMLElement */ static createEl (el, opts_data = {}) { - let node = document.createElement(el); - let props = Object.keys(opts_data); + const node = document.createElement(el); + const props = Object.keys(opts_data); for (let i = 0; i < props.length; i++) { if (props[i] === 'class') { @@ -136,8 +136,8 @@ export default class DOM { return; } - let classes = className.split(','); - let len = classes.length; + const classes = className.split(','); + const len = classes.length; for (let i = 0; i < len; i++) { node.classList.add(classes[i]); @@ -162,9 +162,9 @@ export default class DOM { } static css (selector, styles) { - let node = DOM.query(selector); + const node = DOM.query(selector); - for (let prop in styles) { + for (const prop in styles) { node.style[prop] = styles[prop]; } } @@ -179,7 +179,7 @@ export default class DOM { } static trigger (selector, event, settings) { - let e = DOM._customEvent(event, settings); + const e = DOM._customEvent(event, settings); DOM.query(selector).dispatchEvent(e); } @@ -210,7 +210,7 @@ export default class DOM { } static delegate (ctxt, selector, evt, handler) { - let el = DOM.query(ctxt); + const el = DOM.query(ctxt); el.addEventListener(evt, function (event) { let target = event.target; while (!target.isEqualNode(el)) { diff --git a/src/ui/dom/searchparams.js b/src/ui/dom/searchparams.js index aae063793..7ef229763 100644 --- a/src/ui/dom/searchparams.js +++ b/src/ui/dom/searchparams.js @@ -34,7 +34,7 @@ export default class SearchParams { * @returns {Object} mapping from query param -> value where value is '' if no value is provided */ parse (url) { - let params = {}; + const params = {}; let search = url; if (!search) { @@ -102,16 +102,16 @@ export default class SearchParams { * @return {string} */ toString () { - let string = []; - for (let key in this._params) { + const string = []; + for (const key in this._params) { string.push(`${key}=${SearchParams.encode(this._params[key])}`); } return string.join('&'); } entries () { - let entries = []; - for (let key in this._params) { + const entries = []; + for (const key in this._params) { entries.push([key, this._params[key]]); } return entries; @@ -132,7 +132,7 @@ export default class SearchParams { * @return {string} */ static encode (string) { - let replace = { + const replace = { '!': '%21', "'": '%27', '(': '%28', diff --git a/src/ui/icons/chevron.js b/src/ui/icons/chevron.js index 6fdf8d02d..13043f735 100644 --- a/src/ui/icons/chevron.js +++ b/src/ui/icons/chevron.js @@ -2,5 +2,5 @@ import SVGIcon from './icon.js'; export default new SVGIcon({ name: 'chevron', viewBox: '0 0 7 9', - complexContents: `` + complexContents: '' }); diff --git a/src/ui/icons/kabob.js b/src/ui/icons/kabob.js index e443e7f48..42aee2def 100644 --- a/src/ui/icons/kabob.js +++ b/src/ui/icons/kabob.js @@ -2,5 +2,5 @@ import SVGIcon from './icon.js'; export default new SVGIcon({ name: 'kabob', viewBox: '0 0 3 11', - complexContents: `` + complexContents: '' }); diff --git a/src/ui/rendering/defaulttemplatesloader.js b/src/ui/rendering/defaulttemplatesloader.js index 7ee0fe534..a8523e9c9 100644 --- a/src/ui/rendering/defaulttemplatesloader.js +++ b/src/ui/rendering/defaulttemplatesloader.js @@ -31,7 +31,7 @@ export default class DefaultTemplatesLoader { fetchTemplates () { // If template have already been loaded, do nothing - let node = DOM.query('#yext-answers-templates'); + const node = DOM.query('#yext-answers-templates'); if (node) { return Promise.resolve(); } @@ -39,7 +39,7 @@ export default class DefaultTemplatesLoader { // Inject a script to fetch the compiled templates, // wrapping it a Promise for cleanliness return new Promise((resolve, reject) => { - let script = DOM.createEl('script', { + const script = DOM.createEl('script', { id: 'yext-answers-templates', onload: resolve, onerror: reject, diff --git a/src/ui/rendering/handlebarsrenderer.js b/src/ui/rendering/handlebarsrenderer.js index 905389253..2d79d68b2 100644 --- a/src/ui/rendering/handlebarsrenderer.js +++ b/src/ui/rendering/handlebarsrenderer.js @@ -164,17 +164,17 @@ export default class HandlebarsRenderer extends Renderer { }); this.registerHelper('formatPhoneNumber', function (phoneNumberString) { - var cleaned = ('' + phoneNumberString).replace(/\D/g, ''); - var match = cleaned.match(/^(1|)?(\d{3})(\d{3})(\d{4})$/); + const cleaned = ('' + phoneNumberString).replace(/\D/g, ''); + const match = cleaned.match(/^(1|)?(\d{3})(\d{3})(\d{4})$/); if (match) { - var intlCode = (match[1] ? '+1 ' : ''); + const intlCode = (match[1] ? '+1 ' : ''); return [intlCode, '(', match[2], ') ', match[3], '-', match[4]].join(''); } return null; }); this.registerHelper('assign', function (name, value, options) { - let args = arguments; + const args = arguments; options = args[args.length - 1]; if (!options.data.root) { @@ -201,7 +201,7 @@ export default class HandlebarsRenderer extends Renderer { : pluralText; }); - let self = this; + const self = this; this.registerHelper('processTranslation', function (options) { const pluralizationInfo = {}; diff --git a/src/ui/sass/_base.scss b/src/ui/sass/_base.scss index 90be88c66..bb4ed5a81 100644 --- a/src/ui/sass/_base.scss +++ b/src/ui/sass/_base.scss @@ -11,4 +11,8 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; +} + +.yxt-Answers-component--unmounted { + display: none !important; } \ No newline at end of file diff --git a/src/ui/templates/results/verticalresults.hbs b/src/ui/templates/results/verticalresults.hbs index a905105be..e38df1e89 100644 --- a/src/ui/templates/results/verticalresults.hbs +++ b/src/ui/templates/results/verticalresults.hbs @@ -51,7 +51,7 @@ {{#*inline "results"}}
{{#each results}} -
diff --git a/src/ui/tools/filterutils.js b/src/ui/tools/filterutils.js index 5160e4514..7a0b05644 100644 --- a/src/ui/tools/filterutils.js +++ b/src/ui/tools/filterutils.js @@ -37,7 +37,7 @@ export function findSimpleFiltersWithFieldId (persistedFilter, fieldId) { childFilter => findSimpleFiltersWithFieldId(Filter.from(childFilter), fieldId)); } if (Filter.from(persistedFilter).getFilterKey() === fieldId) { - return [ persistedFilter ]; + return [persistedFilter]; } return []; } diff --git a/src/ui/tools/searchparamsparser.js b/src/ui/tools/searchparamsparser.js index 915b76eb4..9c224d5ae 100644 --- a/src/ui/tools/searchparamsparser.js +++ b/src/ui/tools/searchparamsparser.js @@ -1,7 +1,7 @@ /** @module SearchParamsParser */ export default function buildSearchParameters (searchParameterConfigs) { - let searchParameters = { + const searchParameters = { sectioned: false, fields: [] }; diff --git a/tests/acceptance/acceptancesuites/acceptancesuite.js b/tests/acceptance/acceptancesuites/acceptancesuite.js index 6a8e386b4..1b0eac4d9 100644 --- a/tests/acceptance/acceptancesuites/acceptancesuite.js +++ b/tests/acceptance/acceptancesuites/acceptancesuite.js @@ -128,7 +128,7 @@ fixture`Facets page` .after(shutdownServer) .page`${FACETS_PAGE}`; -test(`Facets load on the page, and can affect the search`, async t => { +test('Facets load on the page, and can affect the search', async t => { const searchComponent = FacetsPage.getSearchComponent(); await searchComponent.submitQuery(); @@ -183,7 +183,7 @@ test(`Facets load on the page, and can affect the search`, async t => { await t.expect(actualResultsCount).eql(initialResultsCount); }); -test(`selecting a sort option and refreshing maintains that sort selection`, async t => { +test('selecting a sort option and refreshing maintains that sort selection', async t => { const searchComponent = FacetsPage.getSearchComponent(); await searchComponent.submitQuery(); diff --git a/tests/acceptance/acceptancesuites/facetsonload.js b/tests/acceptance/acceptancesuites/facetsonload.js index d7b7f9718..c355bf468 100644 --- a/tests/acceptance/acceptancesuites/facetsonload.js +++ b/tests/acceptance/acceptancesuites/facetsonload.js @@ -18,7 +18,7 @@ fixture`Facets page` .after(shutdownServer) .page`${FACETS_ON_LOAD_PAGE}`; -test(`Facets work with back/forward navigation and page refresh`, async t => { +test('Facets work with back/forward navigation and page refresh', async t => { const logger = RequestLogger({ url: /v2\/accounts\/me\/answers\/vertical\/query/ }); @@ -45,10 +45,10 @@ test(`Facets work with back/forward navigation and page refresh`, async t => { await options.toggleOption('Client Delivery'); currentFacets = await getFacetsFromRequest(); const state1 = { - 'c_puppyPreference': [], - 'c_employeeDepartment': [{ 'c_employeeDepartment': { '$eq': 'Client Delivery [SO]' } }], - 'languages': [], - 'specialities': [] + c_puppyPreference: [], + c_employeeDepartment: [{ c_employeeDepartment: { $eq: 'Client Delivery [SO]' } }], + languages: [], + specialities: [] }; await t.expect(currentFacets).eql(state1); logger.clear(); @@ -57,9 +57,9 @@ test(`Facets work with back/forward navigation and page refresh`, async t => { await options.toggleOption('Technology'); currentFacets = await getFacetsFromRequest(); const state2 = { - 'c_employeeDepartment': [ - { 'c_employeeDepartment': { '$eq': 'Client Delivery [SO]' } }, - { 'c_employeeDepartment': { '$eq': 'Technology' } } + c_employeeDepartment: [ + { c_employeeDepartment: { $eq: 'Client Delivery [SO]' } }, + { c_employeeDepartment: { $eq: 'Technology' } } ] }; await t.expect(currentFacets).eql(state2); @@ -69,13 +69,13 @@ test(`Facets work with back/forward navigation and page refresh`, async t => { await options.toggleOption('Frodo'); currentFacets = await getFacetsFromRequest(); const state3 = { - 'c_puppyPreference': [{ 'c_puppyPreference': { '$eq': 'Frodo' } }], - 'c_employeeDepartment': [ - { 'c_employeeDepartment': { '$eq': 'Client Delivery [SO]' } }, - { 'c_employeeDepartment': { '$eq': 'Technology' } } + c_puppyPreference: [{ c_puppyPreference: { $eq: 'Frodo' } }], + c_employeeDepartment: [ + { c_employeeDepartment: { $eq: 'Client Delivery [SO]' } }, + { c_employeeDepartment: { $eq: 'Technology' } } ], - 'languages': [], - 'specialities': [] + languages: [], + specialities: [] }; await t.expect(currentFacets).eql(state3); logger.clear(); diff --git a/tests/acceptance/acceptancesuites/filterboxsuite.js b/tests/acceptance/acceptancesuites/filterboxsuite.js index 8d3b7e5af..9c86cd5e0 100644 --- a/tests/acceptance/acceptancesuites/filterboxsuite.js +++ b/tests/acceptance/acceptancesuites/filterboxsuite.js @@ -22,7 +22,7 @@ fixture`FilterBox page` .after(shutdownServer) .page`${FILTERBOX_PAGE}`; -test(`single option filterbox works with back/forward navigation and page refresh`, async t => { +test('single option filterbox works with back/forward navigation and page refresh', async t => { const radiusFilterLogger = RequestLogger({ url: /v2\/accounts\/me\/answers\/vertical\/query/ }); @@ -60,7 +60,7 @@ test(`single option filterbox works with back/forward navigation and page refres await expectRequestLocationRadiusToEql(radiusFilterLogger, 40233.6); }); -test(`multioption filterbox works with back/forward navigation and page refresh`, async t => { +test('multioption filterbox works with back/forward navigation and page refresh', async t => { const filterBoxLogger = RequestLogger({ url: /v2\/accounts\/me\/answers\/vertical\/query/ }); @@ -130,7 +130,7 @@ test(`multioption filterbox works with back/forward navigation and page refresh` }); }); -test(`locationRadius of 0 is persisted`, async t => { +test('locationRadius of 0 is persisted', async t => { const filterBox = FacetsPage.getStaticFilterBox(); const radiusFilter = await filterBox.getFilterOptions('DISTANCE'); await radiusFilter.toggleOption('0 miles'); diff --git a/tests/acceptance/acceptancesuites/filtersearchsuite.js b/tests/acceptance/acceptancesuites/filtersearchsuite.js index 8bf4a77d6..7409d6037 100644 --- a/tests/acceptance/acceptancesuites/filtersearchsuite.js +++ b/tests/acceptance/acceptancesuites/filtersearchsuite.js @@ -16,7 +16,7 @@ fixture`Facets page` .after(shutdownServer) .page`${FACETS_PAGE}`; -test(`filtersearch works with back/forward navigation and page refresh`, async t => { +test('filtersearch works with back/forward navigation and page refresh', async t => { const expectOnlyFilterTagToEql = async expectedText => { await t.expect(filterTags.count).eql(1); const filterTagText = await filterTags.nth(0).find( @@ -56,7 +56,7 @@ test(`filtersearch works with back/forward navigation and page refresh`, async t await expectOnlyFilterTagToEql('New York City, New York, United States'); }); -test(`pagination works with page navigation after selecting a filtersearch filter`, async t => { +test('pagination works with page navigation after selecting a filtersearch filter', async t => { const expectFilterTagIsNewYork = async () => { await t.expect(filterTags.count).eql(1); const filterTagText = await filterTags.nth(0).find( diff --git a/tests/acceptance/acceptancesuites/universalinitialsearch.js b/tests/acceptance/acceptancesuites/universalinitialsearch.js index adc7dbb0b..a4a7d77c3 100644 --- a/tests/acceptance/acceptancesuites/universalinitialsearch.js +++ b/tests/acceptance/acceptancesuites/universalinitialsearch.js @@ -12,12 +12,12 @@ fixture`Universal page with default initial search` .after(shutdownServer) .page`${UNIVERSAL_INITIAL_SEARCH_PAGE}`; -test(`blank defaultInitialSearch will fire on universal if allowEmptySearch is true`, async t => { +test('blank defaultInitialSearch will fire on universal if allowEmptySearch is true', async t => { await Selector('.yxt-Results').with({ visibilityCheck: true })(); await t.expect(Selector('.yxt-Results').exists).ok(); }); -test(`referrerPageUrl is added to the URL on default initial searches`, async t => { +test('referrerPageUrl is added to the URL on default initial searches', async t => { await Selector('.yxt-Results').with({ visibilityCheck: true })(); const currentSearchParams = await getCurrentUrlParams(); const referrerPageUrl = currentSearchParams.has(StorageKeys.REFERRER_PAGE_URL); diff --git a/tests/acceptance/acceptancesuites/verticalinitialsearch.js b/tests/acceptance/acceptancesuites/verticalinitialsearch.js index 82e1cce72..cca8e4492 100644 --- a/tests/acceptance/acceptancesuites/verticalinitialsearch.js +++ b/tests/acceptance/acceptancesuites/verticalinitialsearch.js @@ -12,7 +12,7 @@ fixture`Vertical page with default initial search` .after(shutdownServer) .page`${FILTERBOX_PAGE}`; -test(`referrerPageUrl is added to the URL on default initial searches`, async t => { +test('referrerPageUrl is added to the URL on default initial searches', async t => { await Selector('.yxt-Results').with({ visibilityCheck: true })(); const currentSearchParams = await getCurrentUrlParams(); const referrerPageUrl = currentSearchParams.has(StorageKeys.REFERRER_PAGE_URL); diff --git a/tests/acceptance/acceptancesuites/xss.js b/tests/acceptance/acceptancesuites/xss.js index 0d3c577e1..f03f92827 100644 --- a/tests/acceptance/acceptancesuites/xss.js +++ b/tests/acceptance/acceptancesuites/xss.js @@ -13,9 +13,9 @@ fixture`Universal page` .after(shutdownServer) .page`${UNIVERSAL_PAGE}`; -test(`a universal page's search bar is protected against xss`, async t => { +test('a universal page\'s search bar is protected against xss', async t => { const searchComponent = UniversalPage.getSearchComponent(); - const xssCode = `window.xssFingerprint = 'gottem!!!'`; + const xssCode = 'window.xssFingerprint = \'gottem!!!\''; const xssAttackQuery = `" { +test('a vertical page\'s search bar is protected against xss', async t => { const searchComponent = VerticalPage.getSearchComponent(); - const xssCode = `window.xssFingerprint = 'gottem!!!'`; + const xssCode = 'window.xssFingerprint = \'gottem!!!\''; const xssAttackQuery = `" { - await percySnapshot(t, `facets page pre search`); + await percySnapshot(t, 'facets page pre search'); }); diff --git a/tests/conf/i18n/runtimecallgeneratorutils.js b/tests/conf/i18n/runtimecallgeneratorutils.js index 95df8a0f6..90e5be065 100644 --- a/tests/conf/i18n/runtimecallgeneratorutils.js +++ b/tests/conf/i18n/runtimecallgeneratorutils.js @@ -6,15 +6,15 @@ describe('generateProcessTranslationJsCall works as expected', () => { const interpolationValues = {}; const actualJsCall = generateProcessTranslationJsCall(translation, interpolationValues); - const expectedJsCall = `ANSWERS.processTranslation('Bonjour', {})`; + const expectedJsCall = 'ANSWERS.processTranslation(\'Bonjour\', {})'; expect(actualJsCall).toEqual(expectedJsCall); }); it('translation with plural form', () => { const translations = { - '0': '[[count]] résultat pour [[verticalName]]', - '1': '[[count]] résultats pour [[verticalName]]' + 0: '[[count]] résultat pour [[verticalName]]', + 1: '[[count]] résultats pour [[verticalName]]' }; const interpolationValues = { count: 'myCount', @@ -22,7 +22,7 @@ describe('generateProcessTranslationJsCall works as expected', () => { }; const actualJsCall = generateProcessTranslationJsCall(translations, interpolationValues, 'myCount'); - const expectedJsCall = `ANSWERS.processTranslation({0:'[[count]] résultat pour [[verticalName]]',1:'[[count]] résultats pour [[verticalName]]'}, {count:myCount,verticalName:verticalName}, myCount)`; + const expectedJsCall = 'ANSWERS.processTranslation({0:\'[[count]] résultat pour [[verticalName]]\',1:\'[[count]] résultats pour [[verticalName]]\'}, {count:myCount,verticalName:verticalName}, myCount)'; expect(actualJsCall).toEqual(expectedJsCall); }); diff --git a/tests/conf/i18n/translatehelpervisitor.js b/tests/conf/i18n/translatehelpervisitor.js index 61373c3f9..b32a24b8c 100644 --- a/tests/conf/i18n/translatehelpervisitor.js +++ b/tests/conf/i18n/translatehelpervisitor.js @@ -5,11 +5,15 @@ const _ = require('lodash'); async function createTranslator () { const locale = 'fr'; - return Translator.create(locale, [], { [locale]: { translation: { - 'Hello': 'Bonjour', - 'result': 'résultat', - 'result_plural': 'résultats' - } } }); + return Translator.create(locale, [], { + [locale]: { + translation: { + Hello: 'Bonjour', + result: 'résultat', + result_plural: 'résultats' + } + } + }); } describe('TranslateHelperVisitor translates visited nodes', () => { @@ -19,7 +23,7 @@ describe('TranslateHelperVisitor translates visited nodes', () => { }); it('static translation', () => { - const template = `{{ translate phrase='Hello' }}`; + const template = '{{ translate phrase=\'Hello\' }}'; const ast = Handlebars.parse(template); new TranslateHelperVisitor(translator).accept(ast); @@ -30,7 +34,7 @@ describe('TranslateHelperVisitor translates visited nodes', () => { }); it('plural translation', () => { - const template = `{{ translate phrase='result' pluralForm='results' count=resultCount }}`; + const template = '{{ translate phrase=\'result\' pluralForm=\'results\' count=resultCount }}'; const ast = Handlebars.parse(template); new TranslateHelperVisitor(translator).accept(ast); @@ -54,7 +58,7 @@ describe('TranslateHelperVisitor translates visited nodes', () => { }); it('a non-translate helper should not be modified', () => { - const template = `{{ yext phrase='Hello' }}`; + const template = '{{ yext phrase=\'Hello\' }}'; const ast = Handlebars.parse(template); const originalAst = _.cloneDeep(ast); diff --git a/tests/conf/i18n/translationresolver.js b/tests/conf/i18n/translationresolver.js index 5fba28f4a..56f120055 100644 --- a/tests/conf/i18n/translationresolver.js +++ b/tests/conf/i18n/translationresolver.js @@ -10,14 +10,18 @@ async function createTranslationResolver () { async function createTranslator () { const locale = 'fr'; - return Translator.create(locale, [], { [locale]: { translation: { - 'Hello': 'Bonjour', - 'result': 'résultat', - 'result_plural': 'résultats', - 'object_noun': 'objet', - 'object_noun_plural': 'objets', - 'mail_noun': 'courrier' - } } }); + return Translator.create(locale, [], { + [locale]: { + translation: { + Hello: 'Bonjour', + result: 'résultat', + result_plural: 'résultats', + object_noun: 'objet', + object_noun_plural: 'objets', + mail_noun: 'courrier' + } + } + }); } describe('TranslationResolver can resolve various TranslationPlaceholders', () => { @@ -51,8 +55,8 @@ describe('TranslationResolver can resolve various TranslationPlaceholders', () = interpolationValues: {} }); const expectedResult = { - '0': 'résultat', - '1': 'résultats' + 0: 'résultat', + 1: 'résultats' }; const resolverResult = translationResolver.resolve(placeholder); expect(resolverResult).toMatchObject(expectedResult); @@ -68,8 +72,8 @@ describe('TranslationResolver can resolve various TranslationPlaceholders', () = interpolationValues: {} }); const expectedResult = { - '0': 'objet', - '1': 'objets' + 0: 'objet', + 1: 'objets' }; const resolverResult = translationResolver.resolve(placeholder); expect(resolverResult).toMatchObject(expectedResult); diff --git a/tests/conf/i18n/translator.js b/tests/conf/i18n/translator.js index 96c5b9333..9065cb83b 100644 --- a/tests/conf/i18n/translator.js +++ b/tests/conf/i18n/translator.js @@ -186,7 +186,8 @@ describe('translations with one plural form (French)', () => { 'View our websites [[name]]'); const expectedResult = { 0: 'Voir notre site web [[name]]', - 1: 'Voir nos sites web [[name]]' }; + 1: 'Voir nos sites web [[name]]' + }; expect(translation).toEqual(expectedResult); }); @@ -197,7 +198,8 @@ describe('translations with one plural form (French)', () => { 'internet web, not spider web'); const expectedResult = { 0: 'Voir notre site web [[name]]', - 1: 'Voir nos sites web [[name]]' }; + 1: 'Voir nos sites web [[name]]' + }; expect(translation).toEqual(expectedResult); }); }); @@ -209,7 +211,8 @@ describe('translations with one plural form (French)', () => { '([[resultsCount]] results)'); const expectedResult = { 0: '([[resultsCount]] résultat)', - 1: '([[resultsCount]] résultats)' }; + 1: '([[resultsCount]] résultats)' + }; expect(translation).toEqual(expectedResult); }); }); diff --git a/tests/core/analytics/analyticsreporter.js b/tests/core/analytics/analyticsreporter.js index 2f031f519..3bf82fce8 100644 --- a/tests/core/analytics/analyticsreporter.js +++ b/tests/core/analytics/analyticsreporter.js @@ -34,7 +34,7 @@ describe('reporting events', () => { expect(mockedBeacon).toBeCalledTimes(1); expect(mockedBeacon).toBeCalledWith( expect.anything(), - expect.objectContaining({ 'data': expectedEvent.toApiEvent() })); + expect.objectContaining({ data: expectedEvent.toApiEvent() })); }); it('includes global options', () => { @@ -45,7 +45,7 @@ describe('reporting events', () => { expect(mockedBeacon).toBeCalledTimes(1); expect(mockedBeacon).toBeCalledWith( expect.anything(), - expect.objectContaining({ 'data': expect.objectContaining({ testOption: 'test' }) })); + expect.objectContaining({ data: expect.objectContaining({ testOption: 'test' }) })); }); it('includes experienceVersion when supplied', () => { @@ -56,7 +56,7 @@ describe('reporting events', () => { expect(mockedBeacon).toBeCalledTimes(1); expect(mockedBeacon).toBeCalledWith( expect.anything(), - expect.objectContaining({ 'data': expect.objectContaining({ experienceVersion: 'PRODUCTION' }) })); + expect.objectContaining({ data: expect.objectContaining({ experienceVersion: 'PRODUCTION' }) })); }); it('doesn\'t send cookies by default', () => { diff --git a/tests/core/filters/filternodefactory.js b/tests/core/filters/filternodefactory.js index f5b8af879..7dbb93331 100644 --- a/tests/core/filters/filternodefactory.js +++ b/tests/core/filters/filternodefactory.js @@ -119,17 +119,17 @@ describe('FilterNodeFactory', () => { filter2 = Filter.from(filter2); const node3 = FilterNodeFactory.or(node1, node2); const filter3 = Filter.from({ - [ FilterCombinators.OR ]: [ filter1, filter2 ] + [FilterCombinators.OR]: [filter1, filter2] }); expect(node3.getFilter()).toEqual(filter3); const node4 = FilterNodeFactory.and(node1, node2); const filter4 = Filter.from({ - [ FilterCombinators.AND ]: [ filter1, filter2 ] + [FilterCombinators.AND]: [filter1, filter2] }); expect(node4.getFilter()).toEqual(filter4); const rootNode = FilterNodeFactory.and(node1, node3, node4); expect(rootNode.getFilter()).toEqual(Filter.from({ - [ FilterCombinators.AND ]: [ filter1, filter3, filter4 ] + [FilterCombinators.AND]: [filter1, filter3, filter4] })); const leafNodes = getLeafNodes(rootNode); @@ -153,7 +153,7 @@ describe('FilterNodeFactory', () => { const orNode = FilterNodeFactory.or(node1, node2, FilterNodeFactory.from({ filter: Filter.empty() })); expect(orNode.children).toHaveLength(2); const expectedFilter = Filter.from({ - [FilterCombinators.OR]: [ filter1, filter2 ] + [FilterCombinators.OR]: [filter1, filter2] }); expect(orNode.getFilter()).toEqual(expectedFilter); }); diff --git a/tests/core/filters/filterregistry.js b/tests/core/filters/filterregistry.js index 199721660..03c706547 100644 --- a/tests/core/filters/filterregistry.js +++ b/tests/core/filters/filterregistry.js @@ -53,12 +53,12 @@ describe('FilterRegistry', () => { const transformedFilter1 = { fieldId: 'c_1', matcher: '$eq', - value: filter1.c_1['$eq'] + value: filter1.c_1.$eq }; const transformedFilter2 = { fieldId: 'c_2', matcher: '$eq', - value: filter2.c_2['$eq'] + value: filter2.c_2.$eq }; registry.setStaticFilterNodes('namespace1', node1); expect(registry.getStaticFilterNodes()).toHaveLength(1); @@ -96,18 +96,18 @@ describe('FilterRegistry', () => { const transformedFilter1 = { fieldId: 'c_1', matcher: '$eq', - value: filter1.c_1['$eq'] + value: filter1.c_1.$eq }; const transformedFilter2 = { fieldId: 'c_2', matcher: '$eq', - value: filter2.c_2['$eq'] + value: filter2.c_2.$eq }; const orNode = FilterNodeFactory.or(node1, node2); registry.setStaticFilterNodes('namespace1', orNode); expect(registry.getStaticFilterNodes()).toHaveLength(1); const expectedFilter1 = { - [ FilterCombinators.OR ]: [ filter1, filter2 ] + [FilterCombinators.OR]: [filter1, filter2] }; expect(orNode.getFilter()).toEqual(expectedFilter1); const expectedTransformedFilter1 = { @@ -145,7 +145,7 @@ describe('FilterRegistry', () => { expectedTransformedFilter1, { combinator: FilterCombinators.AND, - filters: [ transformedFilter1, transformedFilter2 ] + filters: [transformedFilter1, transformedFilter2] }, transformedFilter1 ] @@ -154,7 +154,7 @@ describe('FilterRegistry', () => { }); it('can set facet filter nodes, always overriding previous facets', () => { - registry.setFacetFilterNodes([ 'random_field', 'another_field' ], [node1, node2]); + registry.setFacetFilterNodes(['random_field', 'another_field'], [node1, node2]); const expectedFacets = [ { fieldId: 'random_field', @@ -196,9 +196,9 @@ describe('FilterRegistry', () => { const node3 = FilterNodeFactory.from({ filter: filter3 }); - const facetNodes = [ FilterNodeFactory.or(node1, node3), node2 ]; + const facetNodes = [FilterNodeFactory.or(node1, node3), node2]; - registry.setFacetFilterNodes([ 'random_field', 'another_field' ], facetNodes); + registry.setFacetFilterNodes(['random_field', 'another_field'], facetNodes); const expectedFacets = [ { fieldId: 'random_field', @@ -265,7 +265,7 @@ describe('FilterRegistry', () => { }); it('can clear facet filter nodes', () => { - registry.setFacetFilterNodes([ 'random_field', 'another_field' ], [node1, node2]); + registry.setFacetFilterNodes(['random_field', 'another_field'], [node1, node2]); registry.clearFacetFilterNodes(); expect(registry.getFacetFilterNodes()).toEqual([]); }); @@ -284,13 +284,14 @@ describe('FilterRegistry', () => { }); }); - it('_createFacetsFromFilterNodes', () => { - registry.setFacetFilterNodes([ 'random_field', 'another_field' ], [node1, node2]); + + it('createFacetsFromFilterNodes', () => { + registry.setFacetFilterNodes(['random_field', 'another_field'], [node1, node2]); const expectedFacets = { - 'another_field': [], - 'c_1': [{ 'c_1': { '$eq': '1' } }], - 'c_2': [{ 'c_2': { '$eq': '2' } }], - 'random_field': [] + another_field: [], + c_1: [{ c_1: { $eq: '1' } }], + c_2: [{ c_2: { $eq: '2' } }], + random_field: [] }; expect(registry._createFacetsFromFilterNodes()).toEqual(expectedFacets); }); diff --git a/tests/core/filters/simplefilternode.js b/tests/core/filters/simplefilternode.js index db5ca9d1e..93b80d68c 100644 --- a/tests/core/filters/simplefilternode.js +++ b/tests/core/filters/simplefilternode.js @@ -3,10 +3,10 @@ import FilterNodeFactory from '../../../src/core/filters/filternodefactory'; describe('haveEqualSimpleFilters helper', () => { const joeFilterNode = FilterNodeFactory.from({ - filter: { name: { '$eq': 'joe' } } + filter: { name: { $eq: 'joe' } } }); const bobFilterNode = FilterNodeFactory.from({ - filter: { name: { '$eq': 'bob' } } + filter: { name: { $eq: 'bob' } } }); it('works for equivalent simple filters', () => { @@ -23,8 +23,8 @@ describe('haveEqualSimpleFilters helper', () => { const rangeFilterNode = FilterNodeFactory.from({ filter: { fieldId: { - '$ge': 5, - '$le': 7 + $ge: 5, + $le: 7 } } }); @@ -32,8 +32,8 @@ describe('haveEqualSimpleFilters helper', () => { const rangeFilterNodeReverse = FilterNodeFactory.from({ filter: { fieldId: { - '$le': 7, - '$ge': 5 + $le: 7, + $ge: 5 } } }); @@ -44,8 +44,8 @@ describe('haveEqualSimpleFilters helper', () => { const rangeFilterNode = FilterNodeFactory.from({ filter: { iamDifferent: { - '$ge': 5, - '$le': 7 + $ge: 5, + $le: 7 } } }); @@ -53,8 +53,8 @@ describe('haveEqualSimpleFilters helper', () => { const rangeFilterNodeReverse = FilterNodeFactory.from({ filter: { thanBefore: { - '$le': 7, - '$ge': 5 + $le: 7, + $ge: 5 } } }); @@ -65,9 +65,9 @@ describe('haveEqualSimpleFilters helper', () => { const rangeFilterNode = FilterNodeFactory.from({ filter: { aFieldId: { - '$eq': 5, - '$eq': 5, - '$le': 7 + $eq: 5, + $eq: 5, + $le: 7 } } }); @@ -75,9 +75,9 @@ describe('haveEqualSimpleFilters helper', () => { const rangeFilterNodeReverse = FilterNodeFactory.from({ filter: { aFieldId: { - '$eq': 5, - '$le': 7, - '$le': 7 + $eq: 5, + $le: 7, + $le: 7 } } }); diff --git a/tests/core/models/filter.js b/tests/core/models/filter.js index fef4a8d60..6629e7786 100644 --- a/tests/core/models/filter.js +++ b/tests/core/models/filter.js @@ -3,68 +3,68 @@ import Filter from '../../../src/core/models/filter'; describe('creating filters', () => { it('correctly parses filters from the server', () => { const serverFilter = '{"name": { "$eq": "Billy Bastardi" }}'; - const expectedFilter = new Filter({ name: { '$eq': 'Billy Bastardi' } }); + const expectedFilter = new Filter({ name: { $eq: 'Billy Bastardi' } }); const actualFilter = Filter.fromResponse(serverFilter); expect(actualFilter).toEqual(expectedFilter); }); it('properly ORs filters together', () => { - const filter1 = new Filter({ name: { '$eq': 'Billy Bastardi' } }); - const filter2 = new Filter({ name: { '$eq': 'Jesse Sharps' } }); - const expectedFilter = new Filter({ '$or': [filter1, filter2] }); + const filter1 = new Filter({ name: { $eq: 'Billy Bastardi' } }); + const filter2 = new Filter({ name: { $eq: 'Jesse Sharps' } }); + const expectedFilter = new Filter({ $or: [filter1, filter2] }); const actualFilter = Filter.or(filter1, filter2); expect(actualFilter).toEqual(expectedFilter); }); it('properly ANDs filters together', () => { - const filter1 = new Filter({ name: { '$eq': 'Billy Bastardi' } }); - const filter2 = new Filter({ name: { '$eq': 'Jesse Sharps' } }); - const expectedFilter = new Filter({ '$and': [filter1, filter2] }); + const filter1 = new Filter({ name: { $eq: 'Billy Bastardi' } }); + const filter2 = new Filter({ name: { $eq: 'Jesse Sharps' } }); + const expectedFilter = new Filter({ $and: [filter1, filter2] }); const actualFilter = Filter.and(filter1, filter2); expect(actualFilter).toEqual(expectedFilter); }); it('properly creates "equal to" filters', () => { - const expectedFilter = new Filter({ name: { '$eq': 'Billy Bastardi' } }); + const expectedFilter = new Filter({ name: { $eq: 'Billy Bastardi' } }); const actualFilter = Filter.equal('name', 'Billy Bastardi'); expect(actualFilter).toEqual(expectedFilter); }); it('properly creates "less than" filters', () => { - const expectedFilter = new Filter({ age: { '$lt': 30 } }); + const expectedFilter = new Filter({ age: { $lt: 30 } }); const actualFilter = Filter.lessThan('age', 30); expect(actualFilter).toEqual(expectedFilter); }); it('properly creates "less than or equal to" filters', () => { - const expectedFilter = new Filter({ age: { '$le': 30 } }); + const expectedFilter = new Filter({ age: { $le: 30 } }); const actualFilter = Filter.lessThanEqual('age', 30); expect(actualFilter).toEqual(expectedFilter); }); it('properly creates "greater than" filters', () => { - const expectedFilter = new Filter({ age: { '$gt': 30 } }); + const expectedFilter = new Filter({ age: { $gt: 30 } }); const actualFilter = Filter.greaterThan('age', 30); expect(actualFilter).toEqual(expectedFilter); }); it('properly creates "greater than or equal to" filters', () => { - const expectedFilter = new Filter({ age: { '$ge': 30 } }); + const expectedFilter = new Filter({ age: { $ge: 30 } }); const actualFilter = Filter.greaterThanEqual('age', 30); expect(actualFilter).toEqual(expectedFilter); }); it('properly creates range filters', () => { - const expectedInclusiveFilter = new Filter({ 'age': { '$ge': 30, '$le': 40 } }); - const expectedExclusiveFilter = new Filter({ 'age': { '$gt': 30, '$lt': 40 } }); + const expectedInclusiveFilter = new Filter({ age: { $ge: 30, $le: 40 } }); + const expectedExclusiveFilter = new Filter({ age: { $gt: 30, $lt: 40 } }); const actualInclusiveFilter = Filter.inclusiveRange('age', 30, 40); expect(actualInclusiveFilter).toEqual(expectedInclusiveFilter); @@ -74,14 +74,14 @@ describe('creating filters', () => { }); it('properly creates generic filters from a given matcher', () => { - const expectedFilter = new Filter({ 'myField': { '$myMatcher': 'myValue' } }); + const expectedFilter = new Filter({ myField: { $myMatcher: 'myValue' } }); const actualFilter = Filter._fromMatcher('myField', '$myMatcher', 'myValue'); expect(actualFilter).toEqual(expectedFilter); }); it('can properly parse the key of a filter', () => { - let filter = new Filter({ 'myField': { '$myMatcher': 'myValue' } }); + let filter = new Filter({ myField: { $myMatcher: 'myValue' } }); expect(filter.getFilterKey()).toEqual('myField'); filter = Filter.empty(); expect(filter.getFilterKey()).toBeFalsy(); diff --git a/tests/core/models/highlightedvalue.js b/tests/core/models/highlightedvalue.js index 8e6694e91..c8b01d995 100644 --- a/tests/core/models/highlightedvalue.js +++ b/tests/core/models/highlightedvalue.js @@ -5,13 +5,13 @@ describe('createing highlighted values', () => { const data = { key: 'jesse', value: 'Jesse Sharps', - matchedSubstrings: [ { offset: 8, length: 2 } ] + matchedSubstrings: [{ offset: 8, length: 2 }] }; const expectedResult = 'Jesse Sharps'; const expectedInvertedResult = 'Jesse Sharps'; - let highlightedValue = new HighlightedValue(data); + const highlightedValue = new HighlightedValue(data); const result = highlightedValue.get(); const invertedResult = highlightedValue.getInverted(); @@ -23,13 +23,13 @@ describe('createing highlighted values', () => { const data = { key: 'jesse', value: 'Jesse Sharps', - matchedSubstrings: [ { offset: 7, length: 4 }, { offset: 1, length: 3 } ] + matchedSubstrings: [{ offset: 7, length: 4 }, { offset: 1, length: 3 }] }; const expectedResult = 'Jesse Sharps'; const expectedInvertedResult = 'Jesse Sharps'; - let highlightedValue = new HighlightedValue(data); + const highlightedValue = new HighlightedValue(data); const result = highlightedValue.get(); const invertedResult = highlightedValue.getInverted(); @@ -41,13 +41,13 @@ describe('createing highlighted values', () => { const data = { key: 'jesse', value: 'Jesse', - matchedSubstrings: [ { offset: 0, length: 5 } ] + matchedSubstrings: [{ offset: 0, length: 5 }] }; const expectedResult = 'Jesse'; const expectedInvertedResult = 'Jesse'; - let highlightedValue = new HighlightedValue(data); + const highlightedValue = new HighlightedValue(data); const result = highlightedValue.get(); const invertedResult = highlightedValue.getInverted(); @@ -61,7 +61,7 @@ describe('createing highlighted values', () => { const expectedResult = ''; const expectedInvertedResult = ''; - let highlightedValue = new HighlightedValue(data); + const highlightedValue = new HighlightedValue(data); const result = highlightedValue.get(); const invertedResult = highlightedValue.getInverted(); @@ -73,13 +73,13 @@ describe('createing highlighted values', () => { const data = { key: 'jesse', value: 'Jes\'se Sharps', - matchedSubstrings: [ { offset: 8, length: 4 }, { offset: 1, length: 4 } ] + matchedSubstrings: [{ offset: 8, length: 4 }, { offset: 1, length: 4 }] }; const expectedResult = 'Jes%27se Sharps'; const expectedInvertedResult = 'Jes%27se Sharps'; - let highlightedValue = new HighlightedValue(data); + const highlightedValue = new HighlightedValue(data); const transformFn = (string) => { return string.replace(/'/gi, '%27'); }; diff --git a/tests/core/search/searchdatatransformer.js b/tests/core/search/searchdatatransformer.js index 652706814..df85e7cce 100644 --- a/tests/core/search/searchdatatransformer.js +++ b/tests/core/search/searchdatatransformer.js @@ -63,9 +63,9 @@ describe('forming no results response', () => { resultsCount: 0, facets: [ { - 'fieldId': 'c_features', - 'displayName': 'Features', - 'options': [] + fieldId: 'c_features', + displayName: 'Features', + options: [] } ], allResultsForVertical: { @@ -83,16 +83,16 @@ describe('forming no results response', () => { ], facets: [ { - 'fieldId': 'c_features', - 'displayName': 'Features', - 'options': [ + fieldId: 'c_features', + displayName: 'Features', + options: [ { - 'displayName': 'Dog Friendly', - 'count': 1, - 'selected': false, - 'filter': { - 'c_features': { - '$eq': 'Dog Friendly' + displayName: 'Dog Friendly', + count: 1, + selected: false, + filter: { + c_features: { + $eq: 'Dog Friendly' } } } diff --git a/tests/core/utils/masterswitchapi.js b/tests/core/utils/masterswitchapi.js deleted file mode 100644 index cb3ace3b9..000000000 --- a/tests/core/utils/masterswitchapi.js +++ /dev/null @@ -1,71 +0,0 @@ -import MasterSwitchApi from '../../../src/core/utils/masterswitchapi'; -import Storage from '../../../src/core/storage/storage'; -import HttpRequester from '../../../src/core/http/httprequester'; -import StorageKeys from '../../../src/core/storage/storagekeys'; - -jest.mock('../../../src/core/http/httprequester'); - -describe('checking Answers Status page', () => { - it('behaves correctly when JSON is present and disabled is true', () => { - const mockedResponse = - { json: jest.fn(() => Promise.resolve({ disabled: true })) }; - const mockedRequest = jest.fn(() => Promise.resolve(mockedResponse)); - const masterSwitchApi = createMasterSwitchApi(mockedRequest); - - return masterSwitchApi.isDisabled('abc123', 'someexperience') - .then(isDisabled => expect(isDisabled).toBeTruthy()); - }); - - it('behaves correctly when JSON is present and disabled is false', () => { - const mockedResponse = - { json: jest.fn(() => Promise.resolve({ disabled: false })) }; - const mockedRequest = jest.fn(() => Promise.resolve(mockedResponse)); - const masterSwitchApi = createMasterSwitchApi(mockedRequest); - - return masterSwitchApi.isDisabled('abc123', 'someexperience') - .then(isDisabled => expect(isDisabled).toBeFalsy()); - }); - - it('behaves correctly when status page contains JSON of empty object', () => { - const mockedResponse = { json: jest.fn(() => Promise.resolve({ })) }; - const mockedRequest = jest.fn(() => Promise.resolve(mockedResponse)); - const masterSwitchApi = createMasterSwitchApi(mockedRequest); - - return masterSwitchApi.isDisabled('abc123', 'someexperience') - .then(isDisabled => expect(isDisabled).toBeFalsy()); - }); - - it('behaves correctly when network call results in an error', () => { - const mockedRequest = - jest.fn(() => Promise.reject(new Error('Page does not exist'))); - const masterSwitchApi = createMasterSwitchApi(mockedRequest); - - return masterSwitchApi.isDisabled('abc123', 'someexperience') - .then(isDisabled => expect(isDisabled).toBeFalsy()); - }); - - it('behaves correctly when timeout is reached', () => { - const mockedRequest = - jest.fn(() => new Promise(resolve => setTimeout(resolve, 200))); - const masterSwitchApi = createMasterSwitchApi(mockedRequest); - - return masterSwitchApi.isDisabled('abc123', 'someexperience') - .then(isDisabled => expect(isDisabled).toBeFalsy()); - }); -}); - -/** - * - * @param {Function} mockedRequest - * @returns {MasterSwitchApi} - */ -function createMasterSwitchApi (mockedRequest) { - HttpRequester.mockImplementation(() => { - return { - get: mockedRequest - }; - }); - const storage = new Storage().init(); - storage.set(StorageKeys.SESSIONS_OPT_IN, { value: true }); - return MasterSwitchApi.from('apiKey', 'experienceKey', storage); -} diff --git a/tests/setup/enzymeadapter.js b/tests/setup/enzymeadapter.js index 17b169f6c..289915808 100644 --- a/tests/setup/enzymeadapter.js +++ b/tests/setup/enzymeadapter.js @@ -186,7 +186,7 @@ function toDomRstNode (domElement) { const attr = domElement.attributes; for (let i = 0; i < attr.length; i++) { if (attr[i].name === 'class') { - props['className'] = attr[i].value; + props.className = attr[i].value; continue; } props[attr[i].name] = attr[i].value; diff --git a/tests/ui/components/component.js b/tests/ui/components/component.js index 8dd1c69cd..9b0b5d228 100644 --- a/tests/ui/components/component.js +++ b/tests/ui/components/component.js @@ -15,8 +15,8 @@ const DEFAULT_TEMPLATE = '
This is a default template {{name}}
'; // Our render requires the native handlebars compiler, // and the set of precompiled templates for the components to use const RENDERER = new HandlebarsRenderer({ - '_hb': Handlebars, - 'default': Handlebars.compile(DEFAULT_TEMPLATE) + _hb: Handlebars, + default: Handlebars.compile(DEFAULT_TEMPLATE) }); const COMPONENT_MANAGER = new MockComponentManager(); @@ -32,7 +32,7 @@ describe('rendering component templates', () => { const component = COMPONENT_MANAGER.create('Component', { data: { name: 'Billy' } }); - let wrapper = render(component); + const wrapper = render(component); expect(wrapper.text()).toBe('This is a default template Billy'); }); @@ -42,14 +42,14 @@ describe('rendering component templates', () => { data: { name: 'Adrian' } }); component.setTemplate(customTemplate); - let wrapper = render(component); + const wrapper = render(component); expect(wrapper.text()).toBe('This is a test template Adrian'); }); }); describe('creating subcomponents', () => { it('creates subcomponents based on data attributes during mount', () => { - const template = `
`; + const template = '
'; const component = COMPONENT_MANAGER.create('Component', { data: { child: { name: 'Bowen' } @@ -68,7 +68,7 @@ describe('attaching analytics events', () => { }); it('attaches analytics events based on data attributes during mount', () => { - const template = `
This is a test template
`; + const template = '
This is a test template
'; const component = COMPONENT_MANAGER.create( 'Component', @@ -112,7 +112,7 @@ describe('attaching analytics events', () => { }); it('reports analyticsOptions provided to the component', () => { - const template = `
This is a test template
`; + const template = '
This is a test template
'; const component = COMPONENT_MANAGER.create( 'Component', diff --git a/tests/ui/components/filters/filterboxcomponent.js b/tests/ui/components/filters/filterboxcomponent.js index 7f0f46ea6..0a763d2d9 100644 --- a/tests/ui/components/filters/filterboxcomponent.js +++ b/tests/ui/components/filters/filterboxcomponent.js @@ -154,8 +154,8 @@ describe('filter box component', () => { it('can save simple filternodes', () => { const component = COMPONENT_MANAGER.create('FilterBox', config); mount(component); - let child0 = component._filterComponents[0]; - let child1 = component._filterComponents[1]; + const child0 = component._filterComponents[0]; + const child1 = component._filterComponents[1]; child0._updateOption(0, true); expect(child0.getFilterNode().getFilter()).toEqual(nodes0[0].getFilter()); expect(child0.getFilterNode().getMetadata()).toEqual(nodes0[0].getMetadata()); @@ -169,8 +169,8 @@ describe('filter box component', () => { it('can save combined filternodes', () => { const component = COMPONENT_MANAGER.create('FilterBox', config); mount(component); - let child0 = component._filterComponents[0]; - let child1 = component._filterComponents[1]; + const child0 = component._filterComponents[0]; + const child1 = component._filterComponents[1]; child0._updateOption(0, true); child1._updateOption(0, true); child1._updateOption(3, true); @@ -321,14 +321,14 @@ describe('filter box component', () => { expect(setStaticFilterNodes).toHaveBeenCalledTimes(1); expect(setStaticFilterNodes.mock.calls[0][1]).toMatchObject({ filter: { - 'witcher': { - '$eq': 'cirilla' + witcher: { + $eq: 'cirilla' } }, metadata: { - 'displayValue': 'ciri', - 'fieldName': 'first filter options', - 'filterType': 'filter-type-static' + displayValue: 'ciri', + fieldName: 'first filter options', + filterType: 'filter-type-static' } }); setStaticFilterNodes.mockClear(); diff --git a/tests/ui/components/filters/filteroptionscomponent.js b/tests/ui/components/filters/filteroptionscomponent.js index 1962f7ee4..44ea64699 100644 --- a/tests/ui/components/filters/filteroptionscomponent.js +++ b/tests/ui/components/filters/filteroptionscomponent.js @@ -319,7 +319,7 @@ describe('filter options component', () => { }; expect(setStaticFilterNodes.mock.calls).toHaveLength(0); const component = COMPONENT_MANAGER.create('FilterOptions', config); - let filterNode = component.getFilterNode(); + const filterNode = component.getFilterNode(); expect(filterNode.getFilter()).toEqual(FilterNodeFactory.from().getFilter()); expect(filterNode.getMetadata()).toEqual(FilterNodeFactory.from().getMetadata()); expect(setStaticFilterNodes.mock.calls).toHaveLength(1); diff --git a/tests/ui/components/filters/sortoptionscomponent.js b/tests/ui/components/filters/sortoptionscomponent.js index 3393b1a97..391d53f18 100644 --- a/tests/ui/components/filters/sortoptionscomponent.js +++ b/tests/ui/components/filters/sortoptionscomponent.js @@ -182,7 +182,7 @@ describe('sort options component', () => { const isNoResults = true; component.handleVerticalResultsUpdate(isNoResults); const wrapper = mount(component); - expect(wrapper.text()).toEqual(''); + expect(wrapper.find('.yxt-SortOptions-fieldSet')).toHaveLength(0); }); it('uses the persisted sortBys on load', () => { @@ -192,7 +192,7 @@ describe('sort options component', () => { }; const setSortBys = jest.fn(); COMPONENT_MANAGER.core.setSortBys = setSortBys; - COMPONENT_MANAGER.core.storage.setWithPersist(StorageKeys.SORT_BYS, [ threeOptions[1] ]); + COMPONENT_MANAGER.core.storage.setWithPersist(StorageKeys.SORT_BYS, [threeOptions[1]]); const component = COMPONENT_MANAGER.create('SortOptions', opts); const wrapper = mount(component); expect(setSortBys).toHaveBeenCalledTimes(0); diff --git a/tests/ui/components/map/providers/mapboxmapprovider.js b/tests/ui/components/map/providers/mapboxmapprovider.js index 7965b7044..a7a28cda2 100644 --- a/tests/ui/components/map/providers/mapboxmapprovider.js +++ b/tests/ui/components/map/providers/mapboxmapprovider.js @@ -1,7 +1,7 @@ import { MapBoxMarkerConfig } from '../../../../../src/ui/components/map/providers/mapboxmapprovider'; const map = { tmp: 'test value' }; -const wrapper = ``; +const wrapper = ''; describe('create MapBoxMarkerConfig', () => { it('constructs a MapBoxMarkerConfig with provided options', () => { @@ -14,7 +14,7 @@ describe('create MapBoxMarkerConfig', () => { const expected = { map: map, position: { latitude: 91.234, longitude: 71.578 }, - wrapper: `` + wrapper: '' }; expect(mapboxMarkerConfig).toMatchObject(expected); @@ -110,7 +110,7 @@ describe('converts array of markers into MapboxMarkers', () => { ]; const pinConfig = { - wrapper: `` + wrapper: '' }; const actual = MapBoxMarkerConfig.from(markers, pinConfig, map); @@ -144,7 +144,7 @@ describe('converts array of markers into MapboxMarkers', () => { latitude: 44, longitude: 45 }, - wrapper: `1`, + wrapper: '1', label: '1' }), new MapBoxMarkerConfig({ @@ -153,7 +153,7 @@ describe('converts array of markers into MapboxMarkers', () => { latitude: 44, longitude: 45 }, - wrapper: `2`, + wrapper: '2', label: '2' }) ]; diff --git a/tests/ui/components/map/providers/mapprovider.js b/tests/ui/components/map/providers/mapprovider.js index c93c11809..fd225257d 100644 --- a/tests/ui/components/map/providers/mapprovider.js +++ b/tests/ui/components/map/providers/mapprovider.js @@ -55,12 +55,12 @@ describe('can show/hide map behavior for no results with blank map data', () => const visibleForNoResults = undefined; const mapData = { mapMarkers: [1] }; it('shows map if showEmptyMap is true and tehre is no mapt data', () => { - let showEmptyMap = true; + const showEmptyMap = true; expect(MapProvider.shouldHideMap(null, resultsContext, showEmptyMap, visibleForNoResults)).toBeFalsy(); }); it('hides map if showEmptyMap is false and there is no map data', () => { - let showEmptyMap = false; + const showEmptyMap = false; expect(MapProvider.shouldHideMap(null, resultsContext, showEmptyMap, visibleForNoResults)).toBeTruthy(); }); diff --git a/tests/ui/components/navigation/navigationcomponent.js b/tests/ui/components/navigation/navigationcomponent.js index ba64c726d..b3f6ac4cc 100644 --- a/tests/ui/components/navigation/navigationcomponent.js +++ b/tests/ui/components/navigation/navigationcomponent.js @@ -14,7 +14,7 @@ DOM.setup( beforeEach(() => { // Always reset the DOM before each component render test - let bodyEl = DOM.query('body'); + const bodyEl = DOM.query('body'); DOM.empty(bodyEl); // Create the container that our component will be injected into @@ -190,8 +190,6 @@ describe('navigation tab links are correct', () => { }); describe('navigation tab order', () => { - let COMPONENT_MANAGER; - const defaultConfig = { container: '#test-component', verticalKey: 'verticalKey', @@ -202,7 +200,7 @@ describe('navigation tab order', () => { ] }; - COMPONENT_MANAGER = mockManager(); + const COMPONENT_MANAGER = mockManager(); COMPONENT_MANAGER.getComponentNamesForComponentTypes = () => []; diff --git a/tests/ui/components/results/directanswercomponent.js b/tests/ui/components/results/directanswercomponent.js index 6b4788352..66419b117 100644 --- a/tests/ui/components/results/directanswercomponent.js +++ b/tests/ui/components/results/directanswercomponent.js @@ -51,10 +51,10 @@ describe('types logic works properly', () => { ...defaultConfig, defaultCard: 'default-card', types: { - 'FEATURED_SNIPPET': { + FEATURED_SNIPPET: { cardType: 'documentsearch-standard' }, - 'FIELD_VALUE': { + FIELD_VALUE: { cardType: 'allfields-standard' } } @@ -68,7 +68,7 @@ describe('types logic works properly', () => { ...defaultConfig, defaultCard: 'default-card', types: { - 'FEATURED_SNIPPET': { + FEATURED_SNIPPET: { cardType: 'custom-card' } } @@ -82,7 +82,7 @@ describe('types logic works properly', () => { ...defaultConfig, defaultCard: 'default-card', types: { - 'FIELD_VALUE': { + FIELD_VALUE: { cardType: 'custom-card', cardOverrides: [ { @@ -102,7 +102,7 @@ describe('types logic works properly', () => { ...defaultConfig, defaultCard: 'default-card', types: { - 'FIELD_VALUE': { + FIELD_VALUE: { cardType: 'custom-card', cardOverrides: [ { @@ -126,7 +126,7 @@ describe('types logic works properly', () => { ...defaultConfig, defaultCard: 'default-card', types: { - 'FIELD_VALUE': { + FIELD_VALUE: { cardType: 'custom-card', cardOverrides: [ { @@ -147,7 +147,7 @@ describe('types logic works properly', () => { ...defaultConfig, defaultCard: 'default-card', types: { - 'FIELD_VALUE': { + FIELD_VALUE: { cardType: 'custom-card', cardOverrides: [ { @@ -168,7 +168,7 @@ describe('types logic works properly', () => { ...defaultConfig, defaultCard: 'default-card', types: { - 'FIELD_VALUE': { + FIELD_VALUE: { cardType: 'custom-card' } }, diff --git a/tests/ui/components/results/resultsheadercomponent.js b/tests/ui/components/results/resultsheadercomponent.js index 28d242767..ee6f388b1 100644 --- a/tests/ui/components/results/resultsheadercomponent.js +++ b/tests/ui/components/results/resultsheadercomponent.js @@ -63,11 +63,11 @@ describe('ResultsHeaderComponent\'s applied filters', () => { }); it('works with simpleFilterNodes, removable = false by default', () => { - const simpleFilterNodes = [ node_f0_v0, node_f0_v1, node_f1_v0 ]; + const simpleFilterNodes = [node_f0_v0, node_f0_v1, node_f1_v0]; resultsHeaderComponent.appliedFilterNodes = simpleFilterNodes; const groupedFilters = resultsHeaderComponent._groupAppliedFilters(); expect(Object.keys(groupedFilters)).toHaveLength(2); - expect(groupedFilters['name0']).toEqual([ + expect(groupedFilters.name0).toEqual([ { displayValue: 'display0', dataFilterId: 0, @@ -79,7 +79,7 @@ describe('ResultsHeaderComponent\'s applied filters', () => { removable: false } ]); - expect(groupedFilters['name1']).toEqual([ + expect(groupedFilters.name1).toEqual([ { displayValue: 'display0', dataFilterId: 2, @@ -89,21 +89,21 @@ describe('ResultsHeaderComponent\'s applied filters', () => { }); it('duplicate display values should still be repeated', () => { - const simpleFilterNodes = [ node_f1_v1, node_f1_v1 ]; + const simpleFilterNodes = [node_f1_v1, node_f1_v1]; resultsHeaderComponent.appliedFilterNodes = simpleFilterNodes; const groupedFilters = resultsHeaderComponent._groupAppliedFilters(); expect(Object.keys(groupedFilters)).toHaveLength(1); - expect(groupedFilters['name1']).toHaveLength(2); + expect(groupedFilters.name1).toHaveLength(2); }); it('nlp filter nodes that are duplicates are removed', () => { - const appliedFilterNodes = [ node_f0_v0, node_f1_v0 ]; - const nlpFilterNodes = [ node_f0_v0, node_f0_v1, node_f1_v0 ]; + const appliedFilterNodes = [node_f0_v0, node_f1_v0]; + const nlpFilterNodes = [node_f0_v0, node_f0_v1, node_f1_v0]; resultsHeaderComponent.appliedFilterNodes = appliedFilterNodes; resultsHeaderComponent.nlpFilterNodes = nlpFilterNodes; const groupedFilters = resultsHeaderComponent._groupAppliedFilters(); expect(Object.keys(groupedFilters)).toHaveLength(2); - expect(groupedFilters['name0']).toEqual([ + expect(groupedFilters.name0).toEqual([ { displayValue: 'display0', dataFilterId: 0, @@ -113,7 +113,7 @@ describe('ResultsHeaderComponent\'s applied filters', () => { displayValue: 'display1' } ]); - expect(groupedFilters['name1']).toEqual([ + expect(groupedFilters.name1).toEqual([ { displayValue: 'display0', dataFilterId: 1, @@ -123,12 +123,12 @@ describe('ResultsHeaderComponent\'s applied filters', () => { }); it('can display removable filters', () => { - const simpleFilterNodes = [ node_f0_v0, node_f0_v1, node_f1_v0 ]; + const simpleFilterNodes = [node_f0_v0, node_f0_v1, node_f1_v0]; resultsHeaderComponent._config.removable = true; resultsHeaderComponent.appliedFilterNodes = simpleFilterNodes; const groupedFilters = resultsHeaderComponent._groupAppliedFilters(); expect(Object.keys(groupedFilters)).toHaveLength(2); - expect(groupedFilters['name0']).toEqual([ + expect(groupedFilters.name0).toEqual([ { displayValue: 'display0', dataFilterId: 0, @@ -140,7 +140,7 @@ describe('ResultsHeaderComponent\'s applied filters', () => { removable: true } ]); - expect(groupedFilters['name1']).toEqual([ + expect(groupedFilters.name1).toEqual([ { displayValue: 'display0', dataFilterId: 2, @@ -161,7 +161,7 @@ describe('ResultsHeaderComponent\'s applied filters', () => { COMPONENT_MANAGER = mockManager({ triggerSearch: triggerSearchFn, filterRegistry: { - getAllFilterNodes: () => [ node_f0_v0, node_f0_v1, node_f1_v0 ] + getAllFilterNodes: () => [node_f0_v0, node_f0_v1, node_f1_v0] } }); diff --git a/tests/ui/components/search/searchcomponent.js b/tests/ui/components/search/searchcomponent.js index 33b0547c6..0e80620fa 100644 --- a/tests/ui/components/search/searchcomponent.js +++ b/tests/ui/components/search/searchcomponent.js @@ -13,7 +13,7 @@ describe('SearchBar component', () => { }; let COMPONENT_MANAGER, storage; beforeEach(() => { - let bodyEl = DOM.query('body'); + const bodyEl = DOM.query('body'); DOM.empty(bodyEl); DOM.append(bodyEl, DOM.createEl('div', { id: 'test-component' })); diff --git a/tests/ui/components/search/spellcheckcomponent.js b/tests/ui/components/search/spellcheckcomponent.js index 88b01ca80..631d99ce7 100644 --- a/tests/ui/components/search/spellcheckcomponent.js +++ b/tests/ui/components/search/spellcheckcomponent.js @@ -6,8 +6,6 @@ import SpellCheckComponent from '../../../../src/ui/components/search/spellcheck const COMPONENT_MANAGER = mockManager(); describe('spellcheck redirect links', () => { - let linkUrlParams; - setupDOM(); const component = createSpellcheckComponent(); @@ -20,7 +18,7 @@ describe('spellcheck redirect links', () => { }); const correctedQueryUrl = component.getState('correctedQueryUrl'); - linkUrlParams = new URLSearchParams(correctedQueryUrl); + const linkUrlParams = new URLSearchParams(correctedQueryUrl); it('redirect links contain a query url param', () => { const query = linkUrlParams.get('query'); diff --git a/tests/ui/dom/searchparams.js b/tests/ui/dom/searchparams.js index 57a6994d8..d8156a799 100644 --- a/tests/ui/dom/searchparams.js +++ b/tests/ui/dom/searchparams.js @@ -9,44 +9,44 @@ beforeEach(() => { describe('searchparams parse', () => { it('parses standard url with no params', () => { - let u = new SearchParams('https://www.yext.com/'); + const u = new SearchParams('https://www.yext.com/'); expect(u.get('https://www.yext.com/')).toStrictEqual(''); expect(u.get('askjfhsdkjhfs')).not.toStrictEqual(''); }); it('parses standard url', () => { - let u = new SearchParams('https://www.yext.com/?query=hello'); + const u = new SearchParams('https://www.yext.com/?query=hello'); expect(u.get('query')).toStrictEqual('hello'); }); it('parses standard url with &', () => { - let u = new SearchParams('https://www.yext.com/?query=hello&q=area51'); + const u = new SearchParams('https://www.yext.com/?query=hello&q=area51'); expect(u.get('query')).toStrictEqual('hello'); expect(u.get('q')).toStrictEqual('area51'); }); it('parses url starting with ?', () => { - let u = new SearchParams('?query=hello'); + const u = new SearchParams('?query=hello'); expect(u.get('query')).toStrictEqual('hello'); }); it('parses url starting with ? with &', () => { - let u = new SearchParams('?query=hello&q=area51'); + const u = new SearchParams('?query=hello&q=area51'); expect(u.get('query')).toStrictEqual('hello'); expect(u.get('q')).toStrictEqual('area51'); }); it('parses url without ?', () => { - let u = new SearchParams('query=hello'); + const u = new SearchParams('query=hello'); expect(u.get('query')).toStrictEqual('hello'); }); it('parses url without ? with &', () => { - let u = new SearchParams('query=hello&q=area51'); + const u = new SearchParams('query=hello&q=area51'); expect(u.get('query')).toStrictEqual('hello'); expect(u.get('q')).toStrictEqual('area51'); }); it('parses url with +', () => { - let u = new SearchParams('query=hello+world&q=area51+event'); + const u = new SearchParams('query=hello+world&q=area51+event'); expect(u.get('query')).toStrictEqual('hello world'); expect(u.get('q')).toStrictEqual('area51 event'); }); it('parses query without value', () => { - let u = new SearchParams('query=&q=area51+event'); + const u = new SearchParams('query=&q=area51+event'); expect(u.get('query')).toStrictEqual(''); expect(u.get('q')).toStrictEqual('area51 event'); }); @@ -54,36 +54,36 @@ describe('searchparams parse', () => { describe('searchparams get', () => { it('gets undefined', () => { - let u = new SearchParams('query=hello+world&q=area51'); + const u = new SearchParams('query=hello+world&q=area51'); expect(u.get(undefined)).toStrictEqual(null); }); it('gets null', () => { - let u = new SearchParams('query=hello+world&q=area51'); + const u = new SearchParams('query=hello+world&q=area51'); expect(u.get(null)).toStrictEqual(null); }); it('gets empty string', () => { - let u = new SearchParams('query=hello+world&q=area51'); + const u = new SearchParams('query=hello+world&q=area51'); expect(u.get('')).toStrictEqual(null); }); it('gets empty', () => { - let u = new SearchParams('query=hello+world&q=area51'); + const u = new SearchParams('query=hello+world&q=area51'); expect(u.get()).toStrictEqual(null); }); }); describe('searchparams set', () => { it('sets undefined', () => { - let u = new SearchParams('query=hello+world&q=area51'); + const u = new SearchParams('query=hello+world&q=area51'); u.set(undefined, 'aliens'); expect(u.get('undefined')).toStrictEqual('aliens'); }); it('sets null', () => { - let u = new SearchParams('query=hello+world&q=area51'); + const u = new SearchParams('query=hello+world&q=area51'); u.set(null, 'aliens'); expect(u.get('null')).toStrictEqual('aliens'); }); it('sets empty string', () => { - let u = new SearchParams('query=hello+world&q=area51'); + const u = new SearchParams('query=hello+world&q=area51'); u.set('', 'aliens'); expect(u.get('')).toStrictEqual('aliens'); }); @@ -91,7 +91,7 @@ describe('searchparams set', () => { describe('searchparams encode', () => { it('toStrings correctly', () => { - let u = new SearchParams('http://www.yext.com/?query=hello+world&q=area51'); + const u = new SearchParams('http://www.yext.com/?query=hello+world&q=area51'); expect(u.toString()).toStrictEqual('query=hello+world&q=area51'); }); it('encodes !\'()%20', () => { diff --git a/tests/ui/rendering/handlebarsrenderer.js b/tests/ui/rendering/handlebarsrenderer.js index 40c70b2e2..36fd30ab5 100644 --- a/tests/ui/rendering/handlebarsrenderer.js +++ b/tests/ui/rendering/handlebarsrenderer.js @@ -4,7 +4,7 @@ import Handlebars from 'handlebars'; describe('the handlebars processTranslation helper', () => { const renderer = new HandlebarsRenderer(); renderer.init({ - '_hb': Handlebars + _hb: Handlebars }, 'en'); it('can translate a string phrase', () => { @@ -96,7 +96,7 @@ describe('the handlebars processTranslation helper', () => { it('use locale specified in the renderer init', () => { const rendererWithLocale = new HandlebarsRenderer(); rendererWithLocale.init({ - '_hb': Handlebars + _hb: Handlebars }, 'lt-LT'); const template = `{{processTranslation @@ -115,7 +115,7 @@ describe('the handlebars processTranslation helper', () => { it('locale in the template overrides the one in the init if specified', () => { const rendererWithLocale = new HandlebarsRenderer(); rendererWithLocale.init({ - '_hb': Handlebars + _hb: Handlebars }, 'en'); const template = `{{processTranslation diff --git a/tests/ui/tools/filterutils.js b/tests/ui/tools/filterutils.js index 6b885b1c7..8dbe04aeb 100644 --- a/tests/ui/tools/filterutils.js +++ b/tests/ui/tools/filterutils.js @@ -140,7 +140,7 @@ describe('findSimpleFiltersWithFieldId', () => { const filter1 = Filter.from({ c_aField: { $eq: 5 } }); - expect(findSimpleFiltersWithFieldId(filter1, 'c_aField')).toEqual([ filter1 ]); + expect(findSimpleFiltersWithFieldId(filter1, 'c_aField')).toEqual([filter1]); }); }); diff --git a/tests/ui/tools/taborder.js b/tests/ui/tools/taborder.js index 1465d22cc..3aa76be87 100644 --- a/tests/ui/tools/taborder.js +++ b/tests/ui/tools/taborder.js @@ -34,7 +34,7 @@ describe('core configuration', () => { } ]; - let params = new URLSearchParams('tabOrder=tab2,tab1'); + const params = new URLSearchParams('tabOrder=tab2,tab1'); const defaultOrder = getDefaultTabOrder(tabConfig, params); expect(defaultOrder).toMatchObject(['tab2', 'tab1']); });