npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@searchstax-inc/searchstudio-ux-vue

v4.2.19

Published

Library to build Site Search page

Readme

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

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

Input Widget

Location Widget

Result Widget

Facets Widget

Pagination Widget

SearchFeedback Widget

RelatedSearches Widget

ExternalPromotions 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 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.

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="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