# sitesearch-ux-vue

Library to build Site Search page

## changelog
changelog is documented at CHANGELOG.md

## Installation
npm install following package
`npm install --save @searchstax-inc/searchstudio-ux-vue`

Add following code to `<head>`

```
<script type="text/javascript">
      var _msq = _msq || []; //declare object
      var analyticsBaseUrl = 'https://analytics-us-east.searchstax.co';
      (function () {
        var ms = document.createElement('script');
        ms.type = 'text/javascript';
        ms.src = 'https://static.searchstax.co/studio-js/v3/js/studio-analytics.js';
        var s = document.getElementsByTagName('script')[0];
        s.parentNode.insertBefore(ms, s);
      })();
    </script>
```
## Usage

After importing SearchstaxWrapper component needs to wrap all other components:

```

<SearchstaxWrapper
    :language="sampleConfig.language"
    :model="sampleConfig.model"
    :searchURL="sampleConfig.searchURL"
    :suggesterURL="sampleConfig.suggesterURL"
    :trackApiKey="sampleConfig.trackApiKey"
    :searchAuth="sampleConfig.searchAuth"
    :authType="sampleConfig.authType"
    :router="sampleConfig.router"
    :beforeSearch="sampleConfig.hooks.beforeSearch" // callback function to intercept search object before search is fired
    :afterSearch="sampleConfig.hooks.afterSearch" //  callback function to handle results after search is fired

  >
  // other components will go there

  </SearchstaxWrapper>
```

## Initialization
Initialization object needs to be of type: [ISearchstaxConfig](https://github.com/searchstax/searchstudio-ux-js/blob/main/src/interfaces/connector.interface.ts#L5)

Initialization example
```
sampleConfig = {
    language: "en",
    model: "Main Profile",
    searchURL: "",
    suggesterURL: "",
    trackApiKey: "",
    searchAuth: "",
    authType: "basic",
    router: {
      enabled: true,
      routeName: "searchstax",
      title: (result: ISearchObject) => {
        return "Search results for: " + result.query;
      },
      ignoredKeys: [],
    },
    hooks: {
      beforeSearch: function (props: ISearchObject) {
        const propsCopy = { ...props };
        return propsCopy;
      },
      afterSearch: function (results: ISearchstaxParsedResult[]) {
        const copy = [...results];
        return copy;
      },
    }
  };
```

## Initial layout
Our base theme is designed with this layout in mind but it is optional as all widgets have id parameters and can be attached to any element.
```
<SearchstaxWrapper
    :language="sampleConfig.language"
    :model="sampleConfig.model"
    :searchURL="sampleConfig.searchURL"
    :suggesterURL="sampleConfig.suggesterURL"
    :trackApiKey="sampleConfig.trackApiKey"
    :searchAuth="sampleConfig.searchAuth"
    :authType="sampleConfig.authType"
    :router="sampleConfig.router"
    :beforeSearch="sampleConfig.hooks.beforeSearch" // callback function to intercept search object before search is fired
    :afterSearch="sampleConfig.hooks.afterSearch" //  callback function to handle results after search is fired
  >
    <template #default>
      <div class="searchstax-page-layout-container">
        <SearchstaxInputWidget
          :afterAutosuggest="afterAutosuggest"
          :beforeAutosuggest="beforeAutosuggest"
          :suggestAfterMinChars="3"
        >
        </SearchstaxInputWidget>
        <div class="search-details-container">
          <SearchstaxSearchFeedbackWidget></SearchstaxSearchFeedbackWidget>
          <SearchstaxSortingWidget></SearchstaxSortingWidget>
        </div>
        <div class="searchstax-page-layout-facet-result-container">
          <div class="searchstax-page-layout-facet-container">
            <SearchstaxFacetsWidget
              :facetingType="'or'"
              :itemsPerPageDesktop="3"
              :itemsPerPageMobile="99"
            >
            </SearchstaxFacetsWidget>
          </div>
          <div class="searchstax-page-layout-result-container">
            <div id="searchstax-external-promotions-layout-container"></div>
            <SearchstaxResultWidget :afterLinkClick="afterLinkClick">
            </SearchstaxResultWidget>
            <div id="searchstax-related-searches-container"></div>
            <SearchstaxPaginationWidget>
            </SearchstaxPaginationWidget>
          </div>
        </div>
      </div>
    </template>
  </SearchstaxWrapper>
```

### widgets
Following widgets are available:

[Answer Widget](#answer-widget)

[Input Widget](#input-widget)

[Location Widget](#location-widget)

[Result Widget](#result-widget)

[Facets Widget](#facets-widget)

[Pagination Widget](#pagination-widget)

[SearchFeedback Widget](#searchfeedback-widget)

[RelatedSearches Widget](#relatedsearches-widget)

[ExternalPromotions Widget](#externalpromotions-widget)

[sorting Widget](#sorting-widget)

### Answer Widget ###
SearchStax Site Search solution offers Vue widgets to assist in building your custom search page.

The SearchstaxAnswerWidget component for Vue provides an AI answer widget for your searches.

**Usage**

```
<SearchstaxAnswerWidget :showMoreAfterWordCount="10"></SearchstaxAnswerWidget>
```

**Props**

- showMoreAfterWordCount - number(default 100) determining after how many words UI will show “Show More” view.
- feedbackWidget – an optional object that configures thumbs-up and thumbs-down feedback functionality.

Example of feedbackWidget config:
```
const feedbackConfig = {
    renderFeedbackWidget: true,
    emailOverride: searchstaxEmailOverride,
    thumbsUpValue: 10,
    thumbsDownValue: 0
  }
```


**searchAnswerTemplate**

The templates prop allows customizing the answer UI.

It receives the following props:

- answerData – Data object of type: ISearchstaxAnswerData
- showMore - Handler for exiting the show more view

**Example**

```
<SearchstaxAnswerWidget :showMoreAfterWordCount="100" :feedbackwidget="feedbackConfig">
      <template #answer="{ answerData, showMore }">
        <div v-if="answerData && answerData?.searchExecuted && answerData.shouldShowAnswer">
    <div class="searchstax-answer-wrap">
      <div class="searchstax-answer-icon"></div>
      <div>
        <div
          :class="{
            'searchstax-answer-container': true,
            'searchstax-answer-show-more': answerData.showMoreButtonVisible
          }"
        >
          <div class="searchstax-answer-title">Smart Answers</div>
          <div
            class="searchstax-answer-error"
            v-if="answerData.shouldShowAnswerError"
            v-html="answerData.answerErrorMessage"
          ></div>
          <div class="searchstax-answer-description" v-html="answerData.fullAnswerFormatted"></div>
          <div v-if="answerData.answerLoading" class="searchstax-answer-loading"></div>
        </div>
        <div
          v-if="answerData.showMoreButtonVisible"
          class="searchstax-answer-load-more-button-container"
        >
          <button class="searchstax-answer-load-more-button" @click="showMore">Show More</button>
        </div>
      </div>
      <div
        v-if="!answerData.answerLoading && !answerData.showMoreButtonVisible && answerData.isConversation"
        :class="{
          'searchstax-conversation-container': true,
          'searchstax-conversation-container-full-screen': answerData.isFullScreenConversation
        }"
      >
        <div v-if="answerData.showConversationHeader" class="searchstax-conversation-full-screen-header">
          <button
            type="button"
            class="searchstax-conversation-back-to-results"
            @click="() => exitConversation?.()"
          >
            Back to results
          </button>
          <div class="searchstax-conversation-full-screen-title">Ask SearchStax</div>
        </div>
        <div
          v-if="answerData.showConversationMessages && answerData.conversationMessages?.length"
          class="searchstax-conversation-messages-container"
        >
          <div
            v-for="(message, index) in answerData.conversationMessages"
            :key="message.conversationMessageId ?? index"
            :class="['searchstax-conversation-messages-container-message', message.messageClass]"
            v-html="message.formattedMessage"
          ></div>
        </div>
        <div v-if="answerData.showConversationInput" class="searchstax-conversation-input-container">
          <input
            type="text"
            id="searchstax-conversation-input"
            class="searchstax-conversation-input"
            placeholder="Ask a follow-up question"
            :ref="(el) => setConversationInputRef?.(el as Element | null)"
            @keydown="handleConversationInputKeydown"
          />
          <button
            type="button"
            class="searchstax-conversation-search-icon searchstax-search-icon"
            aria-label="Submit follow-up question"
            @click="handleConversationSubmitClick"
          ></button>
        </div>
      </div>
      <div class="searchstax-answer-footer">
        <div id="feedbackWidgetContainer"></div>
        <div class="searchstax-lightweight-widget-separator-inline"></div>
        <p class="searchstax-disclaimer">Generative AI is Experimental</p>
      </div>
    </div>
  </div>
      </template>
    </SearchstaxAnswerWidget>
```

### Input Widget ###

SearchStax Site Search solution offers a Vue search-input widget to assist with your custom search page.

The SearchstaxInputWidget provides a search input box with autosuggest/autocomplete functionality.

**Usage**
```
<SearchstaxInputWidget
          :afterAutosuggest="afterAutosuggest"
          :beforeAutosuggest="beforeAutosuggest"
          :suggestAfterMinChars="3"
        ></SearchstaxInputWidget>
```

**Props**

- suggestAfterMinChars - default 3. Number of characters needed for autosuggest to start triggering
- beforeAutosuggest - callback function that gets called before firing autosuggest. autosuggestProps are being passed as a property and can be modified, if passed along further search will execute with modified properties, if null is returned then event gets canceled and search never fires.
- afterAutosuggest - callback function that gets called after autosuggest has values but before rendering. It needs to return same type of data but it can be modified.


**inputWidgetTemplate**

The inputTemplate prop allows customizing the input UI.

It receives the following props:

- suggestions – Array of autosuggestion results
- onMouseLeave - Handler for mouse leave event
- onMouseOver - Handler for mouse over event
- onMouseClick - Handler for mouse click event

**example**
```
<SearchstaxInputWidget
          :afterAutosuggest="afterAutosuggest"
          :beforeAutosuggest="beforeAutosuggest"
          :suggestAfterMinChars="3"
        >
          <template #input="{ suggestions, onMouseLeave, onMouseOver, onMouseClick }">
            <div class="searchstax-search-input-wrapper">
    <input
      type="text"
      id="searchstax-search-input"
      class="searchstax-search-input"
      placeholder="SEARCH FOR..."
      aria-label="search"
    />
    <button v-if="hasInputValue" @click="onClearClick" @keyup.enter="onClearClick" id="searchstax-clear-input-action-button" class="searchstax-cross-icon " aria-label="clear input" role="button"></button>
    <span v-if="hasInputValue" id="searchstax-separator-icon" class="searchstax-separator "></span>
    <div
      class="searchstax-autosuggest-container"
      :class="{ hidden: suggestions.length === 0 }"
      @mouseleave="onMouseLeave"
    >
      <div
        class="searchstax-autosuggest-item"
        v-for="suggestion in suggestions"
        :key="suggestion.term"
      >
        <div
          class="searchstax-autosuggest-item-term-container"
          v-html="suggestion.term"
          tabindex="0"
          @mouseover="onMouseOver(suggestion)"
          @click.stop="onMouseClick()"
        ></div>
      </div>
    </div>
  </div>
  <SearchstaxLocationWidget />
  <button
    class="searchstax-spinner-icon"
    id="searchstax-search-input-action-button"
    aria-label="search"
    role="button"
  ></button>
          </template>
        </SearchstaxInputWidget>
```
### Location Widget ###

SearchStax Site Search solution offers a Vue search-location widget to assist with your custom search page.

The SearchstaxLocationWidget provides a location search input with location-based search functionality.

**Usage**

```
const config = {
  appId: "APP_ID",
  relatedSearchesAPIKey: "KEY"
}

const locationWidgetConfig = {
    locationDecode: (term: string): Promise<ISearchstaxLocation> => {
      return new Promise((resolve) => {
        // make a request to google geocoding API to retrieve lat, lon and address

        const geocodingAPIKey = "GEOCODING_API_KEY";
        const geocodingURL = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(
          term
        )}&key=${geocodingAPIKey}`;
        fetch(geocodingURL)
          .then((response) => response.json())
          .then((data) => {
            if (data.status === "OK" && data.results.length > 0) {
              const result = data.results[0];
              const location = {
                lat: result.geometry.location.lat,
                lon: result.geometry.location.lng,
                address: result.formatted_address,
              };
              resolve(location);
            } else {
              resolve({
                address: undefined,
                lat: undefined,
                lon: undefined,
                error: true,
              });
            }
          })
          .catch(() => {
            resolve({
              address: undefined,
              lat: undefined,
              lon: undefined,
              error: true,
            });
          });
      });
    },
    locationDecodeCoordinatesToAddress: (lat: string, lon: string): Promise<ISearchstaxLocation> => {
      return new Promise((resolve) => {
        fetch(
          `https://geocoding-staging.searchstax.co/reverse?location=${lat},${lon}&components=country:US&app_id=${config.appId}`,
          {
            method: "GET",
            headers: {
              Authorization: `Token ${config.relatedSearchesAPIKey}`,
            },
          }
        )
          .then((response) => response.json())
          .then((data) => {
            if (data.status === "OK" && data.results.length > 0) {
              const result = data.results[0];
              resolve({
                address: result.formatted_address,
                lat: lat,
                lon: lon,
                error: false,
              });
            } else {
              resolve({
                address: undefined,
                lat: lat,
                lon: lon,
                error: true,
              });
            }
          })
          .catch(() => {
            resolve({
              address: undefined,
              lat: lat,
              lon: lon,
              error: true,
            });
          });
      });
    },
    locationSearchEnabled: false,
    locationValuesOverride: {
      locationDistanceEnabled: true,
      filterValues: ["any", "1000"],
      filterUnit: "miles",
      locationFilterDefaultValue: "any"
    },
  }
<SearchstaxLocationWidget :locationDecode="locationWidgetConfig.locationDecodeFunction" :locationDecodeCoordinatesToAddress="locationWidgetConfig.locationDecodeCoordinatesToAddress" :locationValuesOverride="locationWidgetConfig.locationValuesOverride" :locationSearchEnabled="locationWidgetConfig.locationSearchEnabled">
</SearchstaxLocationWidget>
```

**Props**

- locationDecode - callback function to override location decoding
- locationDecodeCoordinatesToAddress - callback function to override location decoding

**Template Override**

The searchLocationTemplate prop allows customizing the location input UI.

It receives the following props:

- locationData –  data containing info on when to show certain elements
- inputValue – value of location input
- locationBlur - Handler for location input blur
- radiusChange - Handler for location radius change
- selectValue  - Value of radius select
- inputChange - location input change handler
- locationError - boolean stating if location has error
- getCurrentLocation - Handler for getting current location from browser


**Example**

```
const config = {
  appId: "APP_ID",
  relatedSearchesAPIKey: "KEY"
}

const locationWidgetConfig = {
    locationDecode: (term: string): Promise<ISearchstaxLocation> => {
      return new Promise((resolve) => {
        // make a request to google geocoding API to retrieve lat, lon and address

        const geocodingAPIKey = "GEOCODING_API_KEY";
        const geocodingURL = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(
          term
        )}&key=${geocodingAPIKey}`;
        fetch(geocodingURL)
          .then((response) => response.json())
          .then((data) => {
            if (data.status === "OK" && data.results.length > 0) {
              const result = data.results[0];
              const location = {
                lat: result.geometry.location.lat,
                lon: result.geometry.location.lng,
                address: result.formatted_address,
              };
              resolve(location);
            } else {
              resolve({
                address: undefined,
                lat: undefined,
                lon: undefined,
                error: true,
              });
            }
          })
          .catch(() => {
            resolve({
              address: undefined,
              lat: undefined,
              lon: undefined,
              error: true,
            });
          });
      });
    },
    locationDecodeCoordinatesToAddress: (lat: string, lon: string): Promise<ISearchstaxLocation> => {
      return new Promise((resolve) => {
        fetch(
          `https://geocoding-staging.searchstax.co/reverse?location=${lat},${lon}&components=country:US&app_id=${config.appId}`,
          {
            method: "GET",
            headers: {
              Authorization: `Token ${config.relatedSearchesAPIKey}`,
            },
          }
        )
          .then((response) => response.json())
          .then((data) => {
            if (data.status === "OK" && data.results.length > 0) {
              const result = data.results[0];
              resolve({
                address: result.formatted_address,
                lat: lat,
                lon: lon,
                error: false,
              });
            } else {
              resolve({
                address: undefined,
                lat: lat,
                lon: lon,
                error: true,
              });
            }
          })
          .catch(() => {
            resolve({
              address: undefined,
              lat: lat,
              lon: lon,
              error: true,
            });
          });
      });
    },
    locationSearchEnabled: false,
    locationValuesOverride: {
      locationDistanceEnabled: true,
      filterValues: ["any", "1000"],
      filterUnit: "miles",
      locationFilterDefaultValue: "any"
    },
  }
<div
        class="searchstax-location-input-container"
        data-test-id="searchstax-location-input-container"
      >
        <div class="searchstax-location-input-wrapper">
          <span class="searchstax-location-input-label">NEAR</span>
          <div class="searchstax-location-input-wrapper-inner">
            <input
              type="text"
              id="searchstax-location-input"
              class="searchstax-location-input"
              :class="{ 'searchstax-input-location-error': locationError }"
              placeholder="Zip, Postal Code or City..."
              aria-label="Search Location Input"
              data-test-id="searchstax-location-input"
              @change="inputChange"
              :value="inputValue"
              @blur="locationBlur"
            />
            <button @click="getCurrentLocation" class="searchstax-get-current-location-button">
              Use my current location
            </button>
          </div>
          <span
            v-if="locationData.shouldShowLocationDistanceDropdown"
            class="searchstax-location-input-label"
            >WITHIN</span
          >
          <select
            v-if="locationData.shouldShowLocationDistanceDropdown"
            id="searchstax-location-radius-select"
            class="searchstax-location-radius-select"
            aria-label="Search Location Radius Select"
            data-test-id="searchstax-location-radius-select"
            @change="radiusChange"
            :value="selectValue"
          >
            <option
              v-for="value in locationData.locationSearchDistanceValues"
              :value="value.value"
              :key="value.value"
            >
              {{ value.label }}
            </option>
          </select>
        </div>
      </div>
<SearchstaxLocationWidget :locationDecode="locationWidgetConfig.locationDecode" :locationDecodeCoordinatesToAddress="locationWidgetConfig.locationDecodeCoordinatesToAddress" :locationValuesOverride="locationWidgetConfig.locationValuesOverride" :locationSearchEnabled="locationWidgetConfig.locationSearchEnabled">
    <template
      #location="{
        locationData,
        inputValue,
        locationBlur,
        radiusChange,
        selectValue,
        inputChange,
        locationError,
        getCurrentLocation
      }"
    >
                <div
        class="searchstax-location-input-container"
        data-test-id="searchstax-location-input-container"
      >
        <div class="searchstax-location-input-wrapper">
          <span class="searchstax-location-input-label">NEAR</span>
          <div class="searchstax-location-input-wrapper-inner">
            <input
              type="text"
              id="searchstax-location-input"
              class="searchstax-location-input"
              :class="{ 'searchstax-input-location-error': locationError }"
              placeholder="Zip, Postal Code or City..."
              aria-label="Search Location Input"
              data-test-id="searchstax-location-input"
              @change="inputChange"
              :value="inputValue"
              @blur="locationBlur"
            />
            <button @click="getCurrentLocation" class="searchstax-get-current-location-button">
              Use my current location
            </button>
          </div>
          <span
            v-if="locationData.shouldShowLocationDistanceDropdown"
            class="searchstax-location-input-label"
            >WITHIN</span
          >
          <select
            v-if="locationData.shouldShowLocationDistanceDropdown"
            id="searchstax-location-radius-select"
            class="searchstax-location-radius-select"
            aria-label="Search Location Radius Select"
            data-test-id="searchstax-location-radius-select"
            @change="radiusChange"
            :value="selectValue"
          >
            <option
              v-for="value in locationData.locationSearchDistanceValues"
              :value="value.value"
              :key="value.value"
            >
              {{ value.label }}
            </option>
          </select>
        </div>
      </div>
    </template>
  </SearchstaxLocationWidget>
```

### Result Widget ###

The SearchStax Site Search solution offers a Vue results widget to assist with your custom search page.

The SearchstaxResultsWidget component displays the search results.

**Usage**

```
<SearchstaxResultWidget :afterLinkClick="afterLinkClick"  :renderMethod="'“pagination”'" :resultsPerPage="10"></SearchstaxResultWidget>
```

**Props**

- renderMethod – either “pagination” or “infiniteScroll”.
- resultsPerPage – number of results on a page.
- afterLinkClick – Callback function invoked when a result link is clicked. Allows modifying the result object.

**Result Template Override**

The resultsTemplate prop allows customizing the result UI.

It receives following props:
- searchResults - Array of result items
- resultClicked - Handler for result click events


**No Results Template Override**

The noResultTemplate prop allows customizing the result UI.

It receives following props:

- searchResults - Array of result items
- resultClicked - Handler for result click events
- searchTerm - Search input term
- metadata - Metadata object
- executeSearch - Handler for executing srearch
- store - Main store of app

**Example of default render method**
```
 <SearchstaxResultWidget :afterLinkClick="afterLinkClick">
              <template #results="{ searchResults, resultClicked }">
                <div class="searchstax-search-results" v-if="searchResults && searchResults.length">
    <a
      :key="searchResult.uniqueId"
      v-for="searchResult in searchResults"
      :href="searchResult.url"
      :data-searchstax-unique-result-id="searchResult.uniqueId"
      @click="resultClicked(searchResult, $event)"
      @keyup.enter="resultClicked(searchResult, $event)"
      @keyup.space="resultClicked(searchResult, $event)"
      aria-labelledby="title-{{searchResult.uniqueId}}"
      class="searchstax-result-item-link searchstax-result-item-link-wrapping"
      tabindex="0"
    >
      <div
        class="searchstax-search-result"
        :class="{
          'has-thumbnail': searchResult.thumbnail,
          promoted: searchResult.promoted
        }"
      >
        <div v-if="searchResult.promoted" class="searchstax-search-result-promoted"></div>
        <div
          v-if="searchResult.ribbon"
          v-html="searchResult.ribbon"
          class="searchstax-search-result-ribbon"
        ></div>
        <img
          alt=""
          v-if="searchResult.thumbnail"
          :src="searchResult.thumbnail"
          class="searchstax-thumbnail"
        />
        <div class="searchstax-search-result-title-container">
          <span class="searchstax-search-result-title" :id="'title-' + searchResult.uniqueId" v-html="searchResult.title"></span>
        </div>
        <p
          v-if="searchResult.paths"
          v-html="searchResult.paths"
          tabindex="0"
          class="searchstax-search-result-common"
        ></p>
        <p
          v-if="searchResult.description"
          tabindex="0"
          v-html="searchResult.description"
          class="searchstax-search-result-description searchstax-search-result-common"
        ></p>
        <div :key="unmappedField.key" v-for="unmappedField in searchResult.unmappedFields">
          <div
            v-if="unmappedField.isImage && typeof unmappedField.value === 'string'"
            class="searchstax-search-result-image-container"
          >
            <img alt="" :src="unmappedField.value" class="searchstax-result-image" />
          </div>
          <div v-else>
            <p tabindex="0" class="searchstax-search-result-common" v-html="unmappedField.value"></p>
          </div>
        </div>
      </div>
    </a>
  </div>
              </template>
              <template #noResult="{ searchResults, resultClicked, searchTerm, metadata, executeSearch, store }">
                <div
    v-if="
      searchResults &&
      searchResults.length === 0 &&
      searchTerm !== '' &&
      searchTerm !== '*' &&
      store.searchstax.dataLayer.parsedData.firstSearchHappened
    "
  >
    <div class="searchstax-no-results">
      Showing <strong>no results</strong> for <strong>"{{ searchTerm }}"</strong>
      <br />
      <span v-if="metadata?.spellingSuggestion"
        >Did you mean
        <a
          href="#"
          class="searchstax-suggestion-term"
          @click="executeSearch(metadata?.spellingSuggestion, $event)"
        >
          {{ metadata?.spellingSuggestion }}</a
        >?</span
      >
    </div>
    <ul>
      <li>
        Try searching for search related terms or topics. We offer a wide variety of content to help
        you get the information you need.
      </li>
    </ul>
  </div>
              </template>
            </SearchstaxResultWidget>
```

**Example of infinite scroll and pagination render methods**
```


 <SearchstaxResultWidget :afterLinkClick="afterLinkClick" :renderMethod="'infiniteScroll'" :resultsPerPage="10">
              <template #results="{ searchResults, resultClicked }">
                <div class="searchstax-search-results" v-if="searchResults && searchResults.length">
    <a
      :key="searchResult.uniqueId"
      v-for="searchResult in searchResults"
      :href="searchResult.url"
      :data-searchstax-unique-result-id="searchResult.uniqueId"
      @click="resultClicked(searchResult, $event)"
      @keyup.enter="resultClicked(searchResult, $event)"
      @keyup.space="resultClicked(searchResult, $event)"
      aria-labelledby="title-{{searchResult.uniqueId}}"
      class="searchstax-result-item-link searchstax-result-item-link-wrapping"
      tabindex="0"
    >
      <div
        class="searchstax-search-result"
        :class="{
          'has-thumbnail': searchResult.thumbnail,
          promoted: searchResult.promoted
        }"
      >
        <div v-if="searchResult.promoted" class="searchstax-search-result-promoted"></div>
        <div
          v-if="searchResult.ribbon"
          v-html="searchResult.ribbon"
          class="searchstax-search-result-ribbon"
        ></div>
        <img
          alt=""
          v-if="searchResult.thumbnail"
          :src="searchResult.thumbnail"
          class="searchstax-thumbnail"
        />
        <div class="searchstax-search-result-title-container">
          <span class="searchstax-search-result-title" :id="'title-' + searchResult.uniqueId" v-html="searchResult.title"></span>
        </div>
        <p
          v-if="searchResult.paths"
          v-html="searchResult.paths"
          tabindex="0"
          class="searchstax-search-result-common"
        ></p>
        <p
          v-if="searchResult.description"
          tabindex="0"
          v-html="searchResult.description"
          class="searchstax-search-result-description searchstax-search-result-common"
        ></p>
        <div :key="unmappedField.key" v-for="unmappedField in searchResult.unmappedFields">
          <div
            v-if="unmappedField.isImage && typeof unmappedField.value === 'string'"
            class="searchstax-search-result-image-container"
          >
            <img alt="" :src="unmappedField.value" class="searchstax-result-image" />
          </div>
          <div v-else>
            <p tabindex="0" class="searchstax-search-result-common" v-html="unmappedField.value"></p>
          </div>
        </div>
      </div>
    </a>
  </div>
              </template>
              <template #noResult="{ searchResults, resultClicked, searchTerm, metadata, executeSearch, store }">
                <div
    v-if="
      searchResults &&
      searchResults.length === 0 &&
      searchTerm !== '' &&
      searchTerm !== '*' &&
      store.searchstax.dataLayer.parsedData.firstSearchHappened
    "
  >
    <div class="searchstax-no-results">
      Showing <strong>no results</strong> for <strong>"{{ searchTerm }}"</strong>
      <br />
      <span v-if="metadata?.spellingSuggestion"
        >Did you mean
        <a
          href="#"
          class="searchstax-suggestion-term"
          @click="executeSearch(metadata?.spellingSuggestion, $event)"
        >
          {{ metadata?.spellingSuggestion }}</a
        >?</span
      >
    </div>
    <ul>
      <li>
        Try searching for search related terms or topics. We offer a wide variety of content to help
        you get the information you need.
      </li>
    </ul>
  </div>
              </template>
            </SearchstaxResultWidget>

```



### Pagination Widget ###

The SearchStax Site Search solution offers a Vue pagination widget to assist with you custom search page.

The SearchstaxPaginationWidget component displays pagination controls for search results.

**Usage**

```
<SearchstaxPaginationWidget></SearchstaxPaginationWidget>
```

**Main Template Override**

Main template for the pagination controls.

It receives following props:

- paginationData – Pagination info object
- previousPage – Handler for previous page click
- nextPage - – Handler for next page click


**Infinite Scroll Template Override**

Main template for the pagination controls in infinite scroll mode.

It receives following props:
- isLastPage - boolean, true if its last page
- results - results.length can be used if there are results

**Example**

```
<SearchstaxPaginationWidget>
  <template #pagination="{ paginationData, previousPage, nextPage }">
    <div class="searchstax-pagination-container" v-if="paginationData">
    <div class="searchstax-pagination-content">
      <a
        class="searchstax-pagination-previous"
        :class="paginationData.isFirstPage ? 'disabled' : ''"
        :disabled="paginationData?.isFirstPage"
        @click="previousPage"
        @keyup.enter="previousPage"
        @keyup.space="previousPage"
        tabindex="0"
        id="searchstax-pagination-previous"
      >
        &lt; Previous
      </a>
      <div class="searchstax-pagination-details">
        {{ paginationData?.startResultIndex }} - {{ paginationData?.endResultIndex }} of
        {{ paginationData?.totalResults }}
      </div>
      <a
        class="searchstax-pagination-next"
        :class="paginationData.isLastPage ? 'disabled' : ''"
        :disabled="paginationData?.isLastPage"
        tabindex="0"
        @click="nextPage"
        @keyup.enter="nextPage"
        @keyup.space="nextPage"
        id="searchstax-pagination-next"
        >Next ></a
      >
    </div>
  </div>
  </template>
</SearchstaxPaginationWidget>
// infinite scroll example
<SearchstaxPaginationWidget>
  <template #infiniteScroll="{ nextPage }">
    <div className="searchstax-pagination-container">
    <a
      class="searchstax-pagination-load-more"
      tabindex="0"
      @click="nextPage"
      @keyup.enter="nextPage"
      @keyup.space="nextPage"
      >Load More</a
    >
  </div>
  </template>
</SearchstaxPaginationWidget>
```

### Facets Widget ###

The SearchStax Site Search solution offers a Vue SearchstaxFacetsWidget component to display facets on your custom search page.

**Facet Selection and Order**

Facet lists are configured and ordered on the Site Search [Faceting Tab](https://www.searchstax.com/docs/searchstudio/faceting-tab/).

**Usage**

```
<SearchstaxFacetsWidget
              :facetingType="'or'"
              :itemsPerPageDesktop="3"
              :itemsPerPageMobile="99"
            ></SearchstaxFacetsWidget>
```

**Props**

- facetingType: "and" | "or" | "showUnavailable" | "tabs"; // type that determines how facets will behave
- specificFacets?: string[]; // optional array of facet names that if provided will only render those facets
- itemsPerPageDesktop: number; // default expanded facets for desktop
- itemsPerPageMobile: number; // default expanded facets for mobile
- beforeFacetsRender `(facets: IFacetData[]) => IFacetData[]` — Called with the current facets array before the widget renders. Return the same or a modified array to filter, reorder, or transform facets (e.g., hide certain facets or change their order).


**Main Template Desktop Override**

Main wrapper template for desktop facets display.

It receives following props:
- facetsTemplateDataDesktop -  Facets data object
- isNotDeactivated - Check if facet group is active
- toggleFacetGroup - Toggle facet group active state
- isChecked - Check if facet value is selected
- selectFacet - Handler for facet select
- showMoreLessDesktop - Show more/less facets handler
- facetContainers - Object of facet DOM containers
- updateRefDesktop - Handler for keeping checkbox references


**Main Template Mobile Override**

Main wrapper template for mobile facets display.

It receives following props:
- facetsTemplateDataMobile - Facets data object
- selectedFacetsCheckboxes - Selected facet values
- isNotDeactivated - Check if facet group is active
- toggleFacetGroup - Toggle facet group active state
- isChecked - Check if facet value is selected
- selectFacet - Handler for facet select
- showMoreLessDesktop - Show more/less facets handler
- facetContainers - Object of facet DOM containers
- openOverlay - Handler to open mobile overlay
- unselectFacet - Handler to unselect specific facet
- unselectAll - Handler to unselect all facets
- closeOverlay - Handler to close mobile overlay
- updateRefMobile - Handler for keeping mobile checkbox references


**Example**
```
<SearchstaxFacetsWidget
 :facetingType="'or'"
 :itemsPerPageDesktop="3"
 :itemsPerPageMobile="99"
>
 <template
   #desktopFacets="{ facetsTemplateDataDesktop, isNotDeactivated, toggleFacetGroup, isChecked, selectFacet, showMoreLessDesktop, facetContainers, updateRefDesktop }"
 >
  <div
    v-if="facetsTemplateDataDesktop?.hasResultsOrExternalPromotions"
    class="searchstax-facets-container-desktop"
  >
    <div
      v-for="facet in facetsTemplateDataDesktop.facets"
      :key="facet.name + 'desktop'"
      class="searchstax-facet-container"
      :class="{ active: isNotDeactivated(facet.name) }"
    >
      <div>
        <div class="searchstax-facet-title-container" @click="toggleFacetGroup(facet.name)">
          <div class="searchstax-facet-title">{{ facet.label }}</div>
          <div class="searchstax-facet-title-arrow active"></div>
        </div>
        <div class="searchstax-facet-values-container" aria-live="polite">
          <div
            v-for="(facetValue) in (facet.values as IFacetValueData[])"
            :key="facetValue.value + facetValue.parentName"
            class="searchstax-facet-value-container"
            :class="{ 'searchstax-facet-value-disabled': facetValue.disabled }"
            :ref="(el) => updateRefDesktop(el, facetValue)"
          >
            <div class="searchstax-facet-input">
              <input
                type="checkbox"
                class="searchstax-facet-input-checkbox"
                :checked="isChecked(facetValue)"
                :disabled="facetValue.disabled"
                :aria-label="facetValue.value + ' ' + facetValue.count"
                @click="selectFacet(facetValue.value + '-desktop', $event, facetValue, true)"
              />
            </div>
            <div
              class="searchstax-facet-value-label"
              @click="selectFacet(facetValue.value + '-desktop', $event, facetValue, false)"
            >
              {{ facetValue.value }}
            </div>
            <div
              class="searchstax-facet-value-count"
              @click="selectFacet(facetValue.value + '-desktop', $event, facetValue, false)"
            >
              ({{ facetValue.count }})
            </div>
          </div>
          <div class="searchstax-facet-show-more-container" v-if="facet.hasMoreFacets">
            <div
              class="searchstax-facet-show-more-container"
              @click="showMoreLessDesktop($event, facet)"
              @keyup.space="
                $event.preventDefault();
                showMoreLessDesktop($event, facet)
              "
              @keyup.enter="showMoreLessDesktop($event, facet)"
              tabindex="0"
              role="button"
            >
              <div
                v-if="facet.showingAllFacets"
                class="searchstax-facet-show-less-button searchstax-facet-show-button"
              >
                less
              </div>
              <div v-else class="searchstax-facet-show-more-button searchstax-facet-show-button">
                more
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
 </template>
 <template
   #mobileFacets="{ facetsTemplateDataMobile, selectedFacetsCheckboxes, isNotDeactivated, toggleFacetGroup, isChecked, selectFacet, showMoreLessDesktop, facetContainers, openOverlay, unselectFacet, unselectAll, closeOverlay, updateRefMobile }"
 >
 <div
    class="searchstax-facets-container-mobile"
    v-if="facetsTemplateDataMobile?.hasResultsOrExternalPromotions"
  >
    <div class="searchstax-facets-pills-container">
      <div class="searchstax-facets-pill searchstax-facets-pill-filter-by" @click="openOverlay">
        <div class="searchstax-facets-pill-label">Filter By</div>
      </div>
      <div class="searchstax-facets-pills-selected">
        <div
          class="searchstax-facets-pill searchstax-facets-pill-facets"
          v-for="facet in selectedFacetsCheckboxes"
          :key="String(facet.value)"
          @click="unselectFacet(facet)"
        >
          <div class="searchstax-facets-pill-label">{{ facet.value }} ({{ facet.count }})</div>
          <div class="searchstax-facets-pill-icon-close"></div>
        </div>
      </div>
      <div
        class="searchstax-facets-pill searchstax-clear-filters searchstax-facets-pill-clear-all"
        v-if="selectedFacetsCheckboxes.length"
        @click="unselectAll"
      >
        <div class="searchstax-facets-pill-label">Clear Filters</div>
      </div>
    </div>
    <div
      class="searchstax-facets-mobile-overlay"
      :class="{ 'searchstax-show': (facetsTemplateDataMobile as any)?.overlayOpened }"
    >
      <div class="searchstax-facets-mobile-overlay-header">
        <div class="searchstax-facets-mobile-overlay-header-title">Filter By</div>
        <div class="searchstax-search-close" @click="closeOverlay"></div>
      </div>
      <div class="searchstax-facets-container-mobile">
        <div
          v-for="facet in facetsTemplateDataMobile?.facets"
          :key="facet.name + 'mobile'"
          class="searchstax-facet-container"
          :class="{ active: isNotDeactivated(facet.name) }"
        >
          <div>
            <div class="searchstax-facet-title-container" @click="toggleFacetGroup(facet.name)">
              <div class="searchstax-facet-title">{{ facet.label }}</div>
              <div class="searchstax-facet-title-arrow active"></div>
            </div>
            <div class="searchstax-facet-values-container" aria-live="polite">
              <div
                v-for="(facetValue) in facet.values as IFacetValueData[]"
                :key="facetValue.value + facetValue.parentName"
                class="searchstax-facet-value-container"
                :class="{ 'searchstax-facet-value-disabled': facetValue.disabled }"
                :ref="
                  (el) => {
                    updateRefMobile(el, facetValue)
                  }
                "
              >
                <div class="searchstax-facet-input">
                  <input
                    type="checkbox"
                    class="searchstax-facet-input-checkbox"
                    :checked="isChecked(facetValue)"
                    :disabled="facetValue.disabled"
                    :aria-label="facetValue.value + ' ' + facetValue.count"
                    @click="selectFacet(facetValue.value + '-mobile', $event, facetValue, true)"
                  />
                </div>
                <div
                  class="searchstax-facet-value-label"
                  @click="selectFacet(facetValue.value + '-mobile', $event, facetValue, false)"
                >
                  {{ facetValue.value }}
                </div>
                <div
                  class="searchstax-facet-value-count"
                  @click="selectFacet(facetValue.value + '-mobile', $event, facetValue, false)"
                >
                  ({{ facetValue.count }})
                </div>
              </div>
              <div class="searchstax-facet-show-more-container" v-if="facet.hasMoreFacets">
                <div
                  class="searchstax-facet-show-more-container"
                  @click="showMoreLessMobile($event, facet)"
                  @keyup.space="showMoreLessMobile($event, facet)"
                  @keyup.enter="showMoreLessMobile($event, facet)"
                  tabindex="0"
                >
                  <div
                    v-if="facet.showingAllFacets"
                    class="searchstax-facet-show-less-button searchstax-facet-show-button"
                  >
                    less
                  </div>
                  <div
                    v-else
                    class="searchstax-facet-show-more-button searchstax-facet-show-button"
                  >
                    more
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
      <button class="searchstax-facets-mobile-overlay-done" @click="closeOverlay">Done</button>
    </div>
  </div>
 </template>
</SearchstaxFacetsWidget>
```

### SearchFeedback Widget ###

The SearchStax Site Search solution provides a search feedback widget to support your Vue search pages.

The SearchstaxSearchFeedbackWidget displays search feedback and stats.

**Usage**

```
<SearchstaxSearchFeedbackWidget></SearchstaxSearchFeedbackWidget>
```

**Main Template Override**

Main template for the search feedback message.

It receives following props:
- searchFeedbackData - Feedback data object
- onOriginalQueryClick - Handler for clicking on original query suggestion

**Example**
```
<SearchstaxSearchFeedbackWidget>
            <template #searchFeedback="{ searchFeedbackData, onOriginalQueryClick }">
                <h2
    class="searchstax-feedback-container"
    v-if="
      searchFeedbackData && searchFeedbackData?.searchExecuted && searchFeedbackData?.totalResults
    "
  >
    Showing
    <b>{{ searchFeedbackData.startResultIndex }} - {{ searchFeedbackData.endResultIndex }}</b> of
    <b>{{ searchFeedbackData.totalResults }}</b> results
    <span v-if="searchFeedbackData.searchTerm"
      >for "<b>{{ searchFeedbackData.searchTerm }}</b
      >"
    </span>
    <div class="searchstax-feedback-container-suggested">
      <div v-if="searchFeedbackData.autoCorrectedQuery">
        Search instead for
        <a
          href="#"
          @click.prevent="onOriginalQueryClick($event)"
          class="searchstax-feedback-original-query"
          :aria-label="'Search instead for ' + searchFeedbackData.originalQuery"
          >{{ searchFeedbackData.originalQuery }}</a
        >
      </div>
    </div>
  </h2>
            </template>
          </SearchstaxSearchFeedbackWidget>
```

### RelatedSearches widget ###

The SearchStax Site Search solution offers a Vue widget for displaying related searches.

The SearchstaxRelatedSearchesWidget for Vue component displays related searches.

**Usage**
```
  <SearchstaxRelatedSearchesWidget
              :relatedSearchesURL="config.relatedSearchesURL"
              :relatedSearchesAPIKey="config.relatedSearchesAPIKey"
            ></SearchstaxRelatedSearchesWidget>
```

**Props**

- relatedSearchesURL: API URL for fetching related searches
- relatedSearchesAPIKey?: API key for related searches API

**Main Template Override**

Main template for related searches.

It receives following props:

- relatedData - Related searches data object
- executeSearch - Handler to run new search from related term

**Example**

```
<SearchstaxRelatedSearchesWidget
              :relatedSearchesURL="config.relatedSearchesURL"
              :relatedSearchesAPIKey="config.relatedSearchesAPIKey"
            >
              <template #related="{ relatedData, executeSearch }">
              <div
    class="searchstax-related-searches-container"
    id="searchstax-related-searches-container"
    v-if="relatedData && relatedData?.searchExecuted && relatedData?.hasRelatedSearches"
  >
    Related searches: <span id="searchstax-related-searches"></span>
    <span class="searchstax-related-search" v-if="relatedData.relatedSearches">
      <span
        v-for="related in relatedData.relatedSearches"
        :key="related.related_search"
        @click="executeSearch(related)"
        @keyup.enter="executeSearch(related)"
        @keyup.space="executeSearch(related)"
        tabindex="0"
        class="searchstax-related-search searchstax-related-search-item"
        :aria-label="'Related search: ' + related.related_search"
      >
        {{ related.related_search }}<span v-if="!related.last">,</span>
      </span>
    </span>
  </div>
              </template>
            </SearchstaxRelatedSearchesWidget>
```

### ExternalPromotions widget ###

The SearchStax Site Search solution offers a Vue external-promotions widget for your custom search page.

The SearchstaxExternalPromotionsWidget component displays external promotions fetched from the API.

**Usage**

```
<SearchstaxExternalPromotionsWidget></SearchstaxExternalPromotionsWidget>
```

**Main Template Override**

Main template for external promotions.

It receives following props:

- externalPromotionsData – External promotions data object
- trackClick – Handler for tracking link clicks

**Example**
```
<SearchstaxExternalPromotionsWidget>
              <template #externalPromotions="{ externalPromotionsData, trackClick }">
                <div
    class="searchstax-external-promotions-container"
    id="searchstax-external-promotions-container"
    v-if="
      externalPromotionsData &&
      externalPromotionsData?.searchExecuted &&
      externalPromotionsData?.hasExternalPromotions
    "
  >
    <div
      class="searchstax-external-promotion searchstax-search-result"
      v-for="externalPromotion in externalPromotionsData.externalPromotions"
      :key="externalPromotion.id"
    >
      <div class="icon-elevated"></div>
      <a
        v-if="externalPromotion.url"
        href="{{externalPromotion.url}}"
        @click="trackClick(externalPromotion, $event)"
        class="searchstax-result-item-link"
      ></a>
      <div class="searchstax-search-result-title-container">
        <span class="searchstax-search-result-title">{{ externalPromotion.name }}</span>
      </div>
      <p
        v-if="externalPromotion.description"
        class="searchstax-search-result-description searchstax-search-result-common"
      >
        {{ externalPromotion.description }}
      </p>
      <p
        v-if="externalPromotion.url"
        class="searchstax-search-result-description searchstax-search-result-common"
      >
        {{ externalPromotion.url }}
      </p>
    </div>
  </div>
              </template>
            </SearchstaxExternalPromotionsWidget>
```


### Sorting Widget ###

The SearchStax Site Search solution offers a Vue sorting widget for your custom search page.

The SearchstaxSortingWidget component displays sorting options for search results.

**Usage**

```
<SearchstaxSortingWidget></SearchstaxSortingWidget>
```

**Main Template Override**

Main template for sorting widget.

It receives following props:

- sortingData – Sorting data object
- orderChange – Handler for sorting change
- selectedSorting – Current selected sorting

**Example**

```
<SearchstaxSortingWidget>
            <template #sorting="{ sortingData, orderChange, selectedSorting }">
                <div
    class="searchstax-sorting-container"
    v-if="sortingData && sortingData?.searchExecuted && sortingData?.hasResultsOrExternalPromotions && sortingData.sortOptions.length > 0"
  >
    <label class="searchstax-sorting-label" for="searchstax-search-order-select">Sort By</label>
    <select
      id="searchstax-search-order-select"
      class="searchstax-search-order-select"
      :value="selectedSorting"
      @change="orderChange($event)"
    >
      <option
        v-for="sortOption in sortingData.sortOptions"
        :key="sortOption.key"
        :value="sortOption.key"
      >
        {{ sortOption.value }}
      </option>
    </select>
  </div>
            </template>
          </SearchstaxSortingWidget>
```


## Template overrides
Templates use vue templating.

## STYLING

scss styles can be imported from searchstudio-ux-js
```
 @import './../node_modules/@searchstax-inc/searchstudio-ux-js/dist/styles/scss/mainTheme.scss';
```
css can be taken from

```
./../node_modules/@searchstax-inc/searchstudio-ux-js/dist/styles/mainTheme.css
```