diff --git a/README.md b/README.md index ec1dd4d..9309ebe 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,36 @@ # Sajari Website Search Integration -Our auto-generated search integrations are a quick and easy way to get [Sajari Website Search](https://www.sajari.com/website-search) running on your site. +Our auto-generated search integrations are a quick and easy way to get +[Sajari Website Search](https://www.sajari.com/website-search) running on your +site. -This repository is used in the [Console](https://www.sajari.com/console/collections/install) to generate search interfaces which can be copy-pasted directly into your website. +This repository is used in the +[Console](https://www.sajari.com/console/collections/install) to generate search +interfaces which can be copy-pasted directly into your website. -This website search integration is built using the [Sajari React SDK](https://www.github.com/sajari/sajari-sdk-react). +This website search integration is built using the +[Sajari React SDK](https://www.github.com/sajari/sajari-sdk-react). ## Instructions -We're assuming you've setup an account and have a website collection indexing. If not then you need to [Sign Up](https://www.sajari.com/console/sign-up) and create a website collection to get started. +This integration requires a website collection. You can +[Sign Up](https://www.sajari.com/console/sign-up) and create a website +collection to get started. -From the [Install tab](https://www.sajari.com/console/collections/install) in the Console you can generate a search interface which can be copy-pasted into your site. It's easy to add further customisations using CSS (see [Styling](#styling)), or by changing the JSON config (see [Configuration](#configuration)). +From the [Install tab](https://www.sajari.com/console/collections/install) in +the Console you can generate a search interface which can be copy-pasted into +your site. It's easy to add further customisations using CSS (see +[Styling](#styling)), or by changing the JSON config (see +[Configuration](#configuration)). ![Search interface with tabs](https://cloud.githubusercontent.com/assets/2822/25603841/e50022d4-2f42-11e7-9ac0-3968714b9e1d.png) -The configuration required for this example is given below. For more details, see [Configuration](#configuration). - -```javascript -{ - "project": "your-project", - "collection": "your-collection", - "searchBoxPlaceHolder": "Search", - "attachSearchBox": document.getElementById("search-box"), - "attachSearchResponse": document.getElementById("search-response"), - "pipeline": "website", - "tabFilters": { - "defaultTab": "All", - "tabs": [ - {"title": "All", "filter": ""}, - {"title": "Blog", "filter": "dir1='blog'"} - ] - }, - "results": { - "showImages": false - }, - "values": { - "resultsPerPage": "10", - "q": getUrlParam("q") - }, - "overlay": false -} -``` - ## Styling -The generated interface can be easily styled to fit your website's look and feel, it's also designed to be responsive by default. +The generated interface is designed to be responsive by default, and can be +easily styled to fit your website's look and feel. -Here are a few CSS examples which can be used to override the default layout. +Here are a few CSS examples showing how to override the default layout. ### Brand colors @@ -76,151 +60,264 @@ Source: [sajari.css](./sample-styles/sajari.css) ![Sajari](./sample-styles/sajari.png) -## Configuration +## Integrations -The generated search interfaces are configured using a simple JSON object which contains attributes that control: +There are 4 types of integration: -* [Project/Collection](#projectcollection) -* [Pipeline](#pipeline) -* [Attaching to the DOM](#attaching-to-the-dom) -* [Result Config](#result-config) -* [Search box place holder text](#search-box-placeholder-text) -* [Algorithm parameters](#algorithm-parameters) -* [Tab filters](#tab-filters) +* [_inline_](#inline): search box, results. Interface is embedded directly into + a page (or pages) on your website, for instance a dedicated search page with a + search box + results. -You'll find the configuration object in the snippet generated from the [install page](https://www.sajari.com/console/collections/install). +* [_overlay_](#overlay): full page overlay, search box, results. Interface + appears as an overlay on top of the current page. Can be used to search + without leaving the page. -### Project/Collection +* [_search box_](#search-box): search box. Typical usage includes being embedded + into headers and menus. -The `project` and `collection` attributes set which project/collection combo to query. These can be found in the Console. +* [_dynamic content_](#dynamic-content): results. Typically used to put + pre-baked searches into pages, to show similar or popular content, or article + listings by tag/category etc for landing pages. -```javascript -project: "your-project", -collection: "your-collection", -``` +It's possible to use [multiple integrations](#multiple-integrations) on the same +site/page. For instance: have a search box in the header of your site, which +then redirects to an inline search results page when triggered. + +**NOTE: The code examples in this README assume integrations have been generated +from the [Console](https://www.sajari.com/console/collections/install). +Generated interfaces come with a handful of +[helper functions](#helper-functions) which are referenced in the following +examples.** + +### Inline -### Pipeline +The inline search integration renders a full search interface (input box and +results) inside a webpage. A typical example would be a dedicated search results +page which is linked to/navigated to by search forms on a website. In the +configuration example given below, the search query can be passed to the page +using the query param q. -Pipeline sets the query pipeline to use for the search interface. The default pipeline for website search is `website`. +![inline interface screenshot](https://user-images.githubusercontent.com/2771466/31525575-f22be452-b00c-11e7-94e0-64a52480aea3.png) ```javascript -pipeline: "website", +myUI({ + mode: "inline", // Set the integration mode + project: "", // Set this to your project. + collection: "", // Set this to your collection. + values: { resultsPerPage: "10", q: getUrlParam("q") }, // Default pipeline values + attachSearchBox: document.getElementById("search-box"), // DOM element to render search box. + attachSearchResponse: document.getElementById("search-response"), // DOM element to render search results. + results: { showImages: false }, // Results configuration + pipeline: "website", // Set this to your search pipeline + instantPipeline: "autocomplete", // Set this to your instant pipeline + inputPlaceholder: "Search", // Placeholder text for the input element + maxSuggestions: 5, // Maximum number of suggestions in the search box + tabFilters: {} // Tab configuration +}); ``` -### Attaching to the DOM +### Overlay -The interface can be displayed in two ways, in page or as an overlay. +The overlay search integration renders a search interface on top of existing +pages. Typically this is used on sites that prefer not to navigate users away +from their current page to see results. -To display in page, set the `attachSearchBox` and `attachSearchResponse` values. -These two attributes control which DOM elements the search box and results components will be rendered in. +![overlay interface screenshot](https://user-images.githubusercontent.com/2771466/31525612-3ebe9abc-b00d-11e7-9e2b-1e2f947a717a.png) ```javascript -attachSearchBox: document.getElementById("search-box"), -attachSearchResponse: document.getElementById("search-response"), +myUI({ + mode: "overlay", // Set the integration mode + project: "", + collection: "", + values: { resultsPerPage: "10", q: getUrlParam("q") }, + results: { showImages: false }, + pipeline: "website", + instantPipeline: "autocomplete", + inputPlaceholder: "Search", + autocompleteMaxSuggestions: 5, + inputAutoFocus: true, + tabFilters: {} +}); ``` -To display as an overlay, set the `overlay` value. +### Search Box + +The Search Box integration creates an autocomplete-enabled input box typically +embedded into site headers and menu bars. It performs autocomplete lookups for +each user keypress and can be customised to redirect to a search results page or +trigger custom search actions. + +![search box interface screenshot](https://user-images.githubusercontent.com/2771466/31525645-86e89392-b00d-11e7-91b2-9ddbeb5136a9.png) ```javascript -overlay: true +myUI({ + mode: "search-box", // Set the integration mode + project: "", + collection: "", + instantPipeline: "autocomplete", + inputPlaceholder: "Search", + maxSuggestions: 5, + attachSearchBox: document.getElementById("search-box") +}); ``` -To open the overlay, [publish the show event](#overlay-show-hide) from javascript. +### Dynamic Content -For example, launching the overlay when a button is clicked +The Dynamic Content integration creates a results block using search results +from a pipeline. It can typically be used to show similar or popular pages. -```html - +![dynamic content interface screenshot]() + +```javascript +myUI({ + mode: "dynamic-content", // Set the integration mode + project: "", + collection: "", + pipeline: "website", + attachDynamicContent: document.getElementById("dynamic-content"), + values: { resultsPerPage: "3" }, + results: { showImages: false }, + tracking: false, + searchOnLoad: true +}); ``` -### Result Config +### Helper Functions + +The generated interface code comes with two helper functions: -Result config allows you to modify the result rendering. +* `getUrlParam(x)` extracts a value from a url parameter `x`. +* `setup` creates an object that controls an instance of the integration. -Show images next to search results. +### Multiple Integrations + +Every integration is bound to its own variable (using the `setup` function), so +it's easy to have multiple integrations running on the same page. You an also +have them interact with each other by subscribing and publishing events between +them. ```javascript -results: { - showImages: false -}, +myUI = setup(...); +secondUI = setup(...); + +myUI(...); +secondUI(...); ``` -### Search box placeholder text +## Configuration -Set the placeholder text in the search box. +The generated search interfaces are configured using a JSON object. Generating +an interface from the console will prefill the configuration for you, setting +default values where necessary. -```javascript -searchBoxPlaceHolder: "Search", -``` +**General configuration** -### Algorithm parameters +| Property | Default | Description | +| :--------------- | :-------------------: | :--------------------------------------------------------------------------------- | +| project | `""` | Project to search | +| collection | `""` | Collection to search | +| pipeline | `"website"` | Pipeline to query when pressing enter or clicking a suggestion | +| instantPipeline | `"autocomplete"` | Pipeline to query when typing | +| maxSuggestions | `"5"` | Sets how many autocomplete suggestions are shown in the box below the search input | +| inputPlaceholder | `"Search"` | Placeholder text in the search input box | +| inputAutoFocus | `false` | Focus the searc input html element on initialisation | +| values | _see table below_ | Configuration of the pipeline values | +| results | _see table below_ | Configuration for the search results | -The standard website pipeline defines several algorithm parameters. For example, `q` or `resultsPerPage`. +**Values configuration** + +| Property | Default | Description | +| :------------- | :----------------: | :------------------------------------------------------------------------ | +| q | `getUrlParam("q")` | The initial value of `q` in the pipeline, commonly used as the query text | +| resultsPerPage | `"10"` | Number of results to show per page | + +**Results configuration** + +| Property | Default | Description | +| :--------- | :-----: | :--------------------------------- | +| showImages | `false` | Show images next to search results | + +### Events + +Interfaces are created using `setup` which is included when generating the +interface from the console. ```javascript -values: { - q: getUrlParam("q") // The initial search query will be the value of the query param "q". - resultsPerPage: "10", // Show 10 results per page. -}, +myUI = setup(window, document, "script", "sajari"); ``` -### Events +You can subscribe to events by calling your interface with the `"sub"` value +followed by the pipeline (either `pipeline` or `instantPipeline`) and event +name, then a callback. It takes the form + +```javascript +myUI("sub", ".", callback); +``` -You can subscribe to events by calling your interface with the `"sub"` value followed by the event name and then a callback. +For example, if you are using the default inline interface and want to listen to +the `search-sent` event, you'd write: ```javascript -myUI("sub", "", function() {}); +myUI("sub", "pipeline.search-sent", function(event, values) { + console.log("Search sent with values: ", values); +}); ``` -| Event | Data | Description | -| :-- | :-: | :-- | -| `"search-sent"` | value dictionary | Search request has been sent | -| `"values-updated"` | value dictionary | Value map has updated | -| `"response-updated"` | response object | Response has updated | -| `"page-closed"` | query string | Page is about to be closed | -| `"query-reset"` | query string | Body has changed enough to be considered a new query | -| `"result-clicked"` | query string | Result has been clicked | -| `"search-event"` | query string | Search event | -| `"overlay-show"` | none | Overlay is shown | -| `"overlay-hide"` | none | Overlay is hidden | +| Event | Data | Description | +| :------------------- | :--------------: | :--------------------------------------------------- | +| `"search-sent"` | value dictionary | Search request has been sent | +| `"values-updated"` | value dictionary | Value map has updated | +| `"response-updated"` | response object | Response has updated | +| `"page-closed"` | query string | Page is about to be closed | +| `"query-reset"` | query string | Body has changed enough to be considered a new query | +| `"result-clicked"` | query string | Result has been clicked | +| `"search-event"` | query string | Search event | +| `"overlay-show"` | none | Overlay is shown | +| `"overlay-hide"` | none | Overlay is hidden | You can also publish events which the search interface will pick up. -| Event | Data | Description | -| :-- | :-: | :-- | -| `"values-set"` | value dictionary | Values to merge in | -| `"search-send"` | none | Perform a search | -| `"overlay-show"` | none | Show the overlay | -| `"overlay-hide"` | none | Hide the overlay | +| Event | Data | Description | +| :--------------- | :--------------: | :----------------- | +| `"values-set"` | value dictionary | Values to merge in | +| `"search-send"` | none | Perform a search | +| `"overlay-show"` | none | Show the overlay | +| `"overlay-hide"` | none | Hide the overlay | #### Search Sent -A search has sent and we are now waiting for results. The values used in the search are given to the subscribed function. +A search has sent and we are now waiting for results. The values used in the +search are given to the subscribed function. ```javascript -myUI("sub", "search-sent", function(eventName, values) { +myUI("sub", ".search-sent", function(eventName, values) { console.log("Search sent with ", values); }); ``` #### Values Updated -Values in the interface have been updated. A function is given as the 3rd argument that can be used to merge new values into the value dictionary, it behaves like `pub("values-set", {})` except that it doesn't trigger an event. +Values in the interface have been updated. A function is given as the 3rd +argument that can be used to merge new values into the value dictionary, it +behaves like `pub("values-set", {})` except that it doesn't trigger an event. ```javascript -myUI("sub", "values-updated", function(eventName, values, set) { +myUI("sub", ".values-updated", function(eventName, values, set) { console.log("New values are", values); }); ``` #### Response Updated -The search response has been updated. Caused by a network response being received or results being cleared (usually because the input box has become empty). +The search response has been updated. Caused by a network response being +received or results being cleared (usually because the input box has become +empty). -You can see more info about the `response` object [here](https://github.com/sajari/sajari-sdk-react#listening-for-responses). +You can see more info about the `response` object +[here](https://github.com/sajari/sajari-sdk-react#listening-for-responses). ```javascript -myUI("sub", "response-updated", function(eventName, response) { +myUI("sub", ".response-updated", function(eventName, response) { if (response.isEmpty()) { return; } @@ -234,10 +331,11 @@ myUI("sub", "response-updated", function(eventName, response) { #### Search Event -A search event signals the end of a search session. A common use case of subscribing to them is for reporting. +A search event signals the end of a search session. A common use case of +subscribing to them is for reporting. ```javascript -myUI("sub", "search-event", function (eventName, query) { +myUI("sub", ".search-event", function(eventName, query) { console.log("Search session finished, last query", query); }); ``` @@ -248,14 +346,18 @@ If you'd like more granular events you can also subscribe to these events. function searchFinished(eventName, query) { console.log("Search session finished, last query", query); } -myUI("sub", "page-closed", searchFinished); -myUI("sub", "query-reset", searchFinished); -myUI("sub", "result-clicked", searchFinished); +myUI("sub", ".page-closed", searchFinished); +myUI("sub", ".query-reset", searchFinished); +myUI("sub", ".result-clicked", searchFinished); ``` #### Overlay Show/Hide -Opening and closing the overlay can be done by publishing either the show or hide event. +Opening and closing the overlay can be done by publishing either the show or +hide event. + +**Note: The show and hide events do not have a pipeline prefixing the event +name!** ```javascript myUI("pub", "overlay-show"); @@ -275,10 +377,11 @@ myUI("sub", "overlay-hide", function(eventName) { #### Set Values -Merge new values into the values dictionary. Setting a value to undefined will remove it from the values dictionary. +Merge new values into the values dictionary. Setting a value to undefined will +remove it from the values dictionary. ```javascript -myUI("pub", "values-set", { q: "" }); +myUI("pub", ".values-set", { q: "" }); ``` #### Search @@ -286,12 +389,14 @@ myUI("pub", "values-set", { q: "" }); Search will perform a search request using the values in the value map. ```javascript -myUI("pub", "search-send"); +myUI("pub", ".search-send"); ``` ### Tab filters -Create tabs to filter search results. Tabs are rendered in a UI component when search results are shown. If a tab is clicked then the algorithm parameter `filter` is set to the tab's `filter` attribute. +Create tabs to filter search results. Tabs are rendered in a UI component when +search results are shown. If a tab is clicked then the algorithm parameter +`filter` is set to the tab's `filter` attribute. ```javascript tabFilters: { @@ -310,50 +415,58 @@ For more information on building filter expressions, see [filters](#filters). Filters are used to limit the pages that are returned in a search. -Our crawler extracts common fields when it parses web pages (such as the first and second directories of URLs), which make filtering much easier. It's well worth taking a look at all the extracted fields before you start building filters, as most use cases are quick and easy to get running. +Our crawler extracts common fields when it parses web pages (such as the first +and second directories of URLs), which make filtering much easier. It's well +worth taking a look at all the extracted fields before you start building +filters, as most use cases are quick and easy to get running. Here is a list of the most commonly used fields. * `title` The page title. * `description` The page description. * `image` The URL of an image which corresponds to the page. -* `lang` The language of the page, extracted from the `` element (if present). +* `lang` The language of the page, extracted from the `` element (if + present). -Fields that are based on the URL of the page (ideal for filtering on subsections of a site) are given below. Examples here assume that the page URL is `https://www.sajari.com/blog/year-in-review`: +Fields that are based on the URL of the page (ideal for filtering on subsections +of a site) are given below. Examples here assume that the page URL is +`https://www.sajari.com/blog/year-in-review`: * `url` The full page URL: `https://www.sajari.com/blog/year-in-review` * `dir1` The first directory of the page URL: `blog` * `dir2` The second directory of the page URL: `year-in-review` * `domain` The domain of the page URL: `www.sajari.com` - ### Using Operators -When querying a field, there are a few operators that can be used. Note, all values must be enclosed in single quotation marks, i.e. "field *boost* must be greater than 10" is written as `boost>'10'`. - -| Operator | Description | Example | -| --- | --- | --- | -| Equal To (`=`) | Field is equal to a value (*numeric* or *string*) | `dir1='blog'` | -| Not Equal To (`!=`) | Field is not equal to a value (*numeric* or *string*) | `dir1!='blog'` | -| Greater Than (`>`) | Field is greater than a *numeric* value | `boost>'10'` | -| Greater Than Or Equal To (`>=`) | Field is greater than or equal to a *numeric* value | `boost>='10'` | -| Less Than (`<`) | Field is less than a given *numeric* value | `boost<'50'` | -| Less Than Or Equal To (`<=`) | Field is less than or equal to a given *numeric* value | `boost<'50'` | -| Begins With (`^`) | Field begins with a *string* | `dir1^'bl'` | -| Ends With (`$`) | Field ends with a *string* | `dir1$'og'` | -| Contains (`~`) | Field contains a *string* | `dir1~'blog'` | -| Does Not Contain (`!~`) | Field does not contain a *string* | `dir1!~'blog'` | +When querying a field, there are a few operators that can be used. Note, all +values must be enclosed in single quotation marks, i.e. "field _boost_ must be +greater than 10" is written as `boost>'10'`. + +| Operator | Description | Example | +| ------------------------------- | ------------------------------------------------------ | -------------- | +| Equal To (`=`) | Field is equal to a value (_numeric_ or _string_) | `dir1='blog'` | +| Not Equal To (`!=`) | Field is not equal to a value (_numeric_ or _string_) | `dir1!='blog'` | +| Greater Than (`>`) | Field is greater than a _numeric_ value | `boost>'10'` | +| Greater Than Or Equal To (`>=`) | Field is greater than or equal to a _numeric_ value | `boost>='10'` | +| Less Than (`<`) | Field is less than a given _numeric_ value | `boost<'50'` | +| Less Than Or Equal To (`<=`) | Field is less than or equal to a given _numeric_ value | `boost<'50'` | +| Begins With (`^`) | Field begins with a _string_ | `dir1^'bl'` | +| Ends With (`$`) | Field ends with a _string_ | `dir1$'og'` | +| Contains (`~`) | Field contains a _string_ | `dir1~'blog'` | +| Does Not Contain (`!~`) | Field does not contain a _string_ | `dir1!~'blog'` | ### Combining expressions -It's also possible to build more complex filters by combining field filter expressions with `AND`/`OR` operators, and brackets. +It's also possible to build more complex filters by combining field filter +expressions with `AND`/`OR` operators, and brackets. -| Operator | Description | Example | -| --- | --- | --- | -| `AND` | Both expressions must match | `dir1='blog' AND domain='www.sajari.com'` | -| `OR` | One expression must match | `dir1='blog' OR domain='blog.sajari.com'` | +| Operator | Description | Example | +| -------- | --------------------------- | ----------------------------------------- | +| `AND` | Both expressions must match | `dir1='blog' AND domain='www.sajari.com'` | +| `OR` | One expression must match | `dir1='blog' OR domain='blog.sajari.com'` | -For example, to match pages with language set to `en` on `www.sajari.com` or any page within the `en.sajari.com` domain: +For example, to match pages with language set to `en` on `www.sajari.com` or any +page within the `en.sajari.com` domain: (domain='www.sajari.com' AND lang='en') OR domain='en.sajari.com' - diff --git a/package.json b/package.json index 3f65540..778aea0 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,21 @@ { "name": "website-search", - "version": "1.2.0", + "version": "1.3.0", "private": true, "devDependencies": { - "react-scripts": "1.0.10" + "fs-extra": "^4.0.2", + "inquirer": "^3.3.0", + "react-scripts": "1.0.14" }, "dependencies": { "pubsub-js": "^1.5.7", "react": "15.6.1", "react-dom": "15.6.1", - "sajari-react": "1.3.4", + "sajari-react": "1.5.0", "stackqueue": "1.0.0" }, "scripts": { - "start": "react-scripts start", + "start": "node prepare-start.js", "build": "react-scripts build", "dist": "react-scripts build && node dist.js", "test": "react-scripts test --env=jsdom", diff --git a/prepare-start.js b/prepare-start.js new file mode 100644 index 0000000..29601ec --- /dev/null +++ b/prepare-start.js @@ -0,0 +1,37 @@ +const fs = require("fs-extra"); +const spawn = require("child_process").spawnSync; +const inquirer = require("inquirer"); + +// copy a source file into index.html +const copyAndRun = source => { + try { + fs.copySync("public/" + source + ".html", "public/index.html"); + } catch (e) { + console.log(e); + process.exit(1); + } + spawn("react-scripts", ["start"], { stdio: "inherit" }); + process.exit(0); +}; + +const choices = ["inline", "searchbox", "overlay", "dynamic-content"]; +const choice = process.argv[2]; + +// if the user has supplied an action run it without prompting +if (choices.indexOf(choice) !== -1) { + copyAndRun(choice); +} + +// prompt the user for which action to run +inquirer + .prompt([ + { + type: "list", + name: "choice", + message: "Which integration would you like to run?", + choices + } + ]) + .then(answers => { + copyAndRun(answers.choice); + }); diff --git a/public/dynamic-content.html b/public/dynamic-content.html new file mode 100644 index 0000000..ef98dcd --- /dev/null +++ b/public/dynamic-content.html @@ -0,0 +1,86 @@ + + + + + + Sajari: Dynamic Content Example UI + + + + + +

Dynamic Content

+
+ + + + + + + + + diff --git a/public/index.html b/public/index.html index 70b5149..e69de29 100644 --- a/public/index.html +++ b/public/index.html @@ -1,137 +0,0 @@ - - - - - - Sajari: Website Search Example UI - - - - - -

Overlay

- - - -
- - - - - - -

In Page

- -
- -

Content Block

-
- - - - - - - - diff --git a/public/inline.html b/public/inline.html new file mode 100644 index 0000000..d7e41c3 --- /dev/null +++ b/public/inline.html @@ -0,0 +1,93 @@ + + + + + + Sajari: Inline Website Search Example UI + + + + + +

Inline

+ +
+ + + + + + + + + diff --git a/public/overlay.html b/public/overlay.html new file mode 100644 index 0000000..e5d27ae --- /dev/null +++ b/public/overlay.html @@ -0,0 +1,107 @@ + + + + + + Sajari: Overlay Website Search Example UI + + + + + +

Overlay

+ + + +
+ + + + + + + + + + + + + diff --git a/public/searchbox.html b/public/searchbox.html new file mode 100644 index 0000000..64a432d --- /dev/null +++ b/public/searchbox.html @@ -0,0 +1,89 @@ + + + + + + Sajari: Search Box Website Search Example UI + + + + + +

Search Box

+ + + + + + + + + diff --git a/sample-styles/light.css b/sample-styles/light.css index fe0d25f..91f18ba 100644 --- a/sample-styles/light.css +++ b/sample-styles/light.css @@ -8,21 +8,38 @@ input { .sj-overlay-search { padding-left: 0; padding-right: 0; + background-image: linear-gradient(#202d5f, #614381); + background-size: 100% 120px; } -.sj-search-input-holder-outer { - background-image: linear-gradient(#202d5f, #614381); +.sj-pipeline-response { + background-color: #fff; +} + +.sj-search-holder-outer { height: 66px; - padding: 14px 0px 10px 126px; + padding: 14px 0px 10px 0px; +} + +.sj-overlay .sj-search-holder-outer { + margin-right: 64px; + margin-left: 126px; +} + +.sj-overlay .sj-tabs-container, +.sj-overlay .sj-result-summary, +.sj-overlay .sj-result-list, +.sj-overlay .sj-result-error { + padding-left: 126px; } -.sj-logo { - background-image: url(/img/logo-flat.svg); +.sj-overlay .sj-logo { + background-image: url(https://www.sajari.com/img/logo-flat.svg); background-repeat: no-repeat; + float: left; width: 60px; height: 55px; - background-size: 54px; - float: left; + background-size: 50px; margin-top: 9px; margin-left: 35px; position: absolute; @@ -46,17 +63,6 @@ input { font-size: 14px; } -.sj-tabs-container, -.sj-result-summary, -.sj-result-list, -.sj-result-error { - padding-left: 126px; -} - -.sj-result-list { - max-width: 700px; -} - .sj-result-summary-text { font-size: 14px; } @@ -82,30 +88,29 @@ input { } @media (max-width: 845px) { - .sj-overlay-close { - right: 3%; + .sj-overlay .sj-pipeline-response { + padding: 0px 10px; } - .sj-search-input-holder-outer { + .sj-overlay .sj-search-holder-outer { padding: 69px 0 10px; height: 120px; } - .sj-logo { - margin-left: calc(50vw - 34px); + .sj-overlay .sj-search-holder-outer { + margin-left: 10px; + margin-right: 10px; } - .sj-overlay .sj-search-bar-input-common, - .sj-search-bar-input-common { - width: calc(95vw - 18px); - margin-left: 2.5vw; + .sj-overlay .sj-logo { + margin-left: calc(50% - 30px); + float: none; } - .sj-tabs-container, - .sj-result-summary, - .sj-result-list, - .sj-result-error { - padding-left: 2.5vw; - padding-right: 2.5vw; + .sj-overlay .sj-tabs-container, + .sj-overlay .sj-result-summary, + .sj-overlay .sj-result-list, + .sj-overlay .sj-result-error { + padding-left: 0px; } } diff --git a/sample-styles/light.png b/sample-styles/light.png index 7617f2b..9cc08dd 100644 Binary files a/sample-styles/light.png and b/sample-styles/light.png differ diff --git a/sample-styles/orange.png b/sample-styles/orange.png index a1232d3..8ae1cf6 100644 Binary files a/sample-styles/orange.png and b/sample-styles/orange.png differ diff --git a/sample-styles/sajari.css b/sample-styles/sajari.css index bd642ae..cc8c6d6 100644 --- a/sample-styles/sajari.css +++ b/sample-styles/sajari.css @@ -1,21 +1,37 @@ .sj-overlay-search { padding-left: 0; padding-right: 0; + background-color: #2b3137; } -.sj-search-input-holder-outer { - background-color: #2b3137; +.sj-pipeline-response { + background-color: #fff; +} + +.sj-search-holder-outer { height: 66px; - padding: 14px 0px 10px 126px; + padding: 14px 0px 10px 0px; +} + +.sj-overlay .sj-search-holder-outer { + margin-right: 64px; + margin-left: 126px; } -.sj-logo { - background-image: url(/img/logo-flat.svg); +.sj-overlay .sj-tabs-container, +.sj-overlay .sj-result-summary, +.sj-overlay .sj-result-list, +.sj-overlay .sj-result-error { + padding-left: 126px; +} + +.sj-overlay .sj-logo { + background-image: url(https://www.sajari.com/img/logo-flat.svg); background-repeat: no-repeat; + float: left; width: 60px; height: 55px; background-size: 50px; - float: left; margin-top: 9px; margin-left: 35px; position: absolute; @@ -26,6 +42,10 @@ cursor: pointer; } +.sj-overlay-close:hover { + color: #fff; +} + .sj-tab-active { color: #4285f4; border-bottom: 3px solid #4285f4; @@ -35,19 +55,6 @@ font-size: 14px; } -@media (min-width: 846px) { - .sj-tabs-container, - .sj-result-summary, - .sj-result-list, - .sj-result-error { - padding-left: 126px; - } -} - -.sj-result-list { - max-width: 700px; -} - .sj-result-summary-text { font-size: 14px; } @@ -77,17 +84,25 @@ padding: 0px 10px; } - .sj-overlay .sj-search-bar-input-common { - width: calc(95vw - 18px); - margin-left: 2.5vw; - } - - .sj-search-input-holder-outer { + .sj-overlay .sj-search-holder-outer { padding: 69px 0 10px; height: 120px; } - .sj-logo { - margin-left: calc(50vw - 34px); + .sj-overlay .sj-search-holder-outer { + margin-left: 10px; + margin-right: 10px; + } + + .sj-overlay .sj-logo { + margin-left: calc(50% - 30px); + float: none; + } + + .sj-overlay .sj-tabs-container, + .sj-overlay .sj-result-summary, + .sj-overlay .sj-result-list, + .sj-overlay .sj-result-error { + padding-left: 0px; } } diff --git a/sample-styles/sajari.png b/sample-styles/sajari.png index 42fffa2..7b83158 100644 Binary files a/sample-styles/sajari.png and b/sample-styles/sajari.png differ diff --git a/src/ContentBlockResponse.js b/src/ContentBlockResponse.js deleted file mode 100644 index 3513947..0000000 --- a/src/ContentBlockResponse.js +++ /dev/null @@ -1,69 +0,0 @@ -import React from "react"; - -import { Results, Result, TokenLink } from "sajari-react/ui/results"; -import { responseUpdatedEvent } from "sajari-react/controllers"; - -class ContentBlockResult extends React.Component { - render() { - const { values, token } = this.props; - return ( -
- - {values.title} -

- {values.title} -

-
-
- ); - } -} - -class ContentBlockResponse extends React.Component { - constructor(props) { - super(props); - - this.state = { response: props.pipeline.getResponse() }; - } - - componentDidMount() { - this.removeResponseListener = this.props.pipeline.listen( - responseUpdatedEvent, - this.responseUpdated - ); - } - - componentWillUnmount() { - this.removeResponseListener(); - } - - responseUpdated = response => { - this.setState({ response }); - }; - - render() { - const { config, pipeline, values } = this.props; - const { response } = this.state; - - if (response.isEmpty()) { - return null; - } - - const resultsConfig = config.results || {}; - const resultRenderer = resultsConfig.showImages - ? ContentBlockResult - : Result; - return ( -
- -
-
- ); - } -} - -export default ContentBlockResponse; diff --git a/src/DynamicContentResponse.js b/src/DynamicContentResponse.js new file mode 100644 index 0000000..674f3d9 --- /dev/null +++ b/src/DynamicContentResponse.js @@ -0,0 +1,16 @@ +import React from "react"; + +import { Results, Result, ImageResult } from "sajari-react/ui/results"; + +const DynamicContentResponse = props => { + const { config, pipeline } = props; + const resultsConfig = config.results || {}; + const resultRenderer = resultsConfig.showImages ? ImageResult : Result; + return ( +
+ +
+ ); +}; + +export default DynamicContentResponse; diff --git a/src/InPage.js b/src/InPage.js deleted file mode 100644 index 67bbec7..0000000 --- a/src/InPage.js +++ /dev/null @@ -1,21 +0,0 @@ -import React from "react"; - -import { AutocompleteInput } from "sajari-react/ui/text"; - -class InPage extends React.Component { - render() { - const { pipeline, values, config } = this.props; - return ( -
-
- -
- ); - } -} - -export default InPage; diff --git a/src/Inline.js b/src/Inline.js new file mode 100644 index 0000000..6b0fcb5 --- /dev/null +++ b/src/Inline.js @@ -0,0 +1,29 @@ +import React from "react"; + +import Input from "./Input"; + +class Inline extends React.Component { + render() { + const { + pipeline, + values, + instantPipeline, + instantValues, + config + } = this.props; + return ( +
+
+ +
+ ); + } +} + +export default Inline; diff --git a/src/Input.js b/src/Input.js new file mode 100644 index 0000000..2077740 --- /dev/null +++ b/src/Input.js @@ -0,0 +1,58 @@ +import React from "react"; + +import { + AutocompleteDropdownBase, + Input as SDKInput +} from "sajari-react/ui/text"; + +class Input extends React.Component { + handleUserForceSearch = query => { + const { pipeline, values, instantPipeline, instantValues } = this.props; + return { + values: values || instantValues, + pipeline: pipeline || instantPipeline + }; + }; + + render() { + const { + config, + pipeline, + values, + instantPipeline, + instantValues + } = this.props; + const { inputPlaceholder, inputAutoFocus, maxSuggestions } = config; + + // if there's no instant pipeline use non instant input component + if (!instantPipeline) { + return ( +
+
+ +
+
+ ); + } + + return ( + + ); + } +} + +export default Input; diff --git a/src/Overlay.js b/src/Overlay.js index 14961bc..1e0fbb3 100644 --- a/src/Overlay.js +++ b/src/Overlay.js @@ -1,8 +1,8 @@ import React from "react"; import { Overlay as OverlayFrame, Close } from "sajari-react/ui/overlay"; -import { AutocompleteInput } from "sajari-react/ui/text"; +import Input from "./Input"; import SearchResponse from "./SearchResponse"; class Overlay extends React.Component { @@ -18,22 +18,30 @@ class Overlay extends React.Component { } render() { - const { pipeline, values, config, tabsFilter } = this.props; + const { + instantPipeline, + instantValues, + pipeline, + values, + config, + tabsFilter + } = this.props; return (
- ); diff --git a/src/index.js b/src/index.js index e68d53d..0db1032 100644 --- a/src/index.js +++ b/src/index.js @@ -8,10 +8,11 @@ import { flush } from "stackqueue"; import { selectionUpdatedEvent, Filter, - CombineFilters, + Pipeline, Values, + CombineFilters, + NoTracking, valuesUpdatedEvent, - Pipeline, responseUpdatedEvent, searchSentEvent, pageClosedAnalyticsEvent, @@ -21,15 +22,10 @@ import { import loaded from "./loaded"; import Overlay from "./Overlay"; -import InPage from "./InPage"; +import Inline from "./Inline"; import SearchResponse from "./SearchResponse"; -import ContentBlockResponse from "./ContentBlockResponse"; - -import "sajari-react/ui/overlay/Overlay.css"; -import "sajari-react/ui/text/AutocompleteInput.css"; -import "sajari-react/ui/facets/Tabs.css"; -import "sajari-react/ui/results/Results.css"; -import "sajari-react/ui/results/Paginator.css"; +import Input from "./Input"; +import DynamicContentResponse from "./DynamicContentResponse"; import "./styles.css"; @@ -62,149 +58,60 @@ const error = message => { } }; -const checkConfig = config => { - if (!config.project) { - error("'project' not set in config"); - return false; - } - if (!config.collection) { - error("'collection' not set in config"); - return false; - } - if (!config.pipeline) { - error("'pipeline' not set in config"); - return false; - } - return true; -}; - -const initOverlay = (config, pipeline, values, pub, sub, tabsFilter) => { - const setOverlayControls = controls => { - const show = () => { - document.getElementsByTagName("body")[0].style.overflow = "hidden"; - controls.show(); - }; - const hide = () => { - document.getElementsByTagName("body")[0].style.overflow = ""; - values.set({ q: undefined, "q.override": undefined }); - pipeline.clearResponse(values.get()); - if (config.tabFilters && config.tabFilters.defaultTab) { - disableTabFacetSearch = true; - tabsFilter.set(config.tabFilters.defaultTab); - disableTabFacetSearch = false; - } - controls.hide(); - }; - sub(integrationEvents.overlayShow, show); - sub(integrationEvents.overlayHide, hide); - return { show, hide }; - }; - - // Create a container to render the overlay into - const overlayContainer = document.createElement("div"); - overlayContainer.id = "sj-overlay-holder"; - document.body.appendChild(overlayContainer); - - // Set up global overlay values - document.addEventListener("keydown", e => { - if (e.keyCode === ESCAPE_KEY_CODE) { - pub(integrationEvents.overlayHide); - } - }); - - ReactDOM.render( - , - overlayContainer - ); -}; - -const initInPage = (config, pipeline, values, tabsFilter) => { - ReactDOM.render( - , - config.attachSearchBox - ); - ReactDOM.render( - , - config.attachSearchResponse - ); -}; - -const initContentBlock = (config, pipeline, values, tabsFilter) => { - ReactDOM.render( - , - config.attachContentBlock - ); -}; - -const initInterface = (config, pub, sub) => { - if (!checkConfig(config)) { - return; - } - - const pipeline = new Pipeline( - config.project, - config.collection, - config.pipeline, - undefined, - config.disableGA ? [] : undefined - ); - +const connectPubSub = ( + pub, + sub, + eventPrefix, + pipeline, + values, + connectAnalytics = true +) => { pipeline.listen(searchSentEvent, values => { - pub(integrationEvents.searchSent, values); + pub(`${eventPrefix}.${integrationEvents.searchSent}`, values); }); pipeline.listen(responseUpdatedEvent, response => { - pub(integrationEvents.responseUpdated, response); - }); - - const values = new Values(); - values.listen(valuesUpdatedEvent, (changes, set) => { - if (!changes.page && values.get().page !== "1") { - set({ page: "1" }); - } + pub(`${eventPrefix}.${integrationEvents.responseUpdated}`, response); }); values.listen(valuesUpdatedEvent, (changes, set) => { - pub(integrationEvents.valuesUpdated, changes, set); + pub(`${eventPrefix}.${integrationEvents.valuesUpdated}`, changes, set); }); - sub(integrationEvents.valuesSet, (_, newValues) => { + sub(`${eventPrefix}.${integrationEvents.valuesSet}`, (_, newValues) => { values.set(newValues); }); - sub(integrationEvents.searchSend, () => { + sub(`${eventPrefix}.${integrationEvents.searchSend}`, () => { pipeline.search(values.get()); }); + // Reset page on search values changed + values.listen(valuesUpdatedEvent, (changes, set) => { + if (!changes.page && values.get().page !== "1") { + set({ page: "1" }); + } + }); + + if (!connectAnalytics) { + return; + } const analytics = pipeline.getAnalytics(); analytics.listen(pageClosedAnalyticsEvent, body => { - pub(integrationEvents.pageClosed, body); - pub(integrationEvents.searchEvent, body); + pub(`${eventPrefix}.${integrationEvents.pageClosed}`, body); + pub(`${eventPrefix}.${integrationEvents.searchEvent}`, body); }); analytics.listen(bodyResetAnalyticsEvent, body => { - pub(integrationEvents.queryReset, body); - pub(integrationEvents.searchEvent, body); + pub(`${eventPrefix}.${integrationEvents.queryReset}`, body); + pub(`${eventPrefix}.${integrationEvents.searchEvent}`, body); }); analytics.listen(resultClickedAnalyticsEvent, body => { - pub(integrationEvents.resultClicked, body); - pub(integrationEvents.searchEvent, body); + pub(`${eventPrefix}.${integrationEvents.resultClicked}`, body); + pub(`${eventPrefix}.${integrationEvents.searchEvent}`, body); }); +}; +const setUpTabsFilters = (config, pipeline, values) => { + // Set up tab filters let tabsFilter; if (config.tabFilters && config.tabFilters.defaultTab) { const opts = {}; @@ -250,31 +157,280 @@ const initInterface = (config, pub, sub) => { ); values.set({ filter: () => filter.filter() }); + // Perform a search if the q parameter is set const query = Boolean(config.values.q); if (query) { + values.set({ q: config.values.q }); + // this might be important ;) + // instantPipeline.getValues().set({ q: config.values.q }); pipeline.search(values.get()); } - if (config.overlay) { - initOverlay(config, pipeline, values, pub, sub, tabsFilter); - if (query) { - pub(integrationEvents.overlayShow); - } - return; + return tabsFilter; +}; + +const initSearchbox = (config, pub, sub) => { + if (!config.instantPipeline) { + throw new Error( + "no instantPipeline found, searchbox requires an instantPipeline" + ); } - if (config.attachSearchBox && config.attachSearchResponse) { - initInPage(config, pipeline, values, tabsFilter); - return; + + if (!config.attachSearchBox) { + throw new Error( + "no render target found, searchbox requires attachSearchBox to be set" + ); } - if (config.attachContentBlock) { - initContentBlock(config, pipeline, values, tabsFilter); - return; + + const dummyPipeline = new Pipeline( + config.project, + config.collection, + "", + new NoTracking(), + [] + ); + dummyPipeline.search = values => { + pub(`pipeline.${integrationEvents.searchSent}`, values); + }; + const dummyValues = new Values(); + const instantPipeline = new Pipeline( + config.project, + config.collection, + config.instantPipeline, + new NoTracking(), + [] + ); + const instantValues = new Values(); + + connectPubSub( + pub, + sub, + "instantPipeline", + instantPipeline, + instantValues, + false + ); + + ReactDOM.render( + , + config.attachSearchBox + ); +}; + +const initDynamicContent = (config, pub, sub) => { + if (!config.pipeline) { + throw new Error( + "no pipeline found, dynamic-content interface requires a pipeline" + ); + } + + const [tracking, adapters] = config.tracking + ? [undefined, config.disableGA ? [] : undefined] + : [new NoTracking(), []]; + + const pipeline = new Pipeline( + config.project, + config.collection, + config.pipeline, + tracking, + adapters + ); + const values = new Values(); + connectPubSub(pub, sub, "pipeline", pipeline, values); + values.set(config.values); + + if (config.searchOnLoad) { + pipeline.search(values.get()); } - error( - "no render mode found, need to specify either overlay or attachSearchBox and attachSearchResponse in config" + + ReactDOM.render( + , + config.attachDynamicContent ); }; +const initInline = (config, pub, sub) => { + if (!config.pipeline && !config.instantPipeline) { + throw new Error( + "no pipeline found, inline interface requires at least 1 pipeline" + ); + } + if (!config.attachSearchBox) { + throw new Error( + "no render target found, inline interface requires attachSearchBox to be set" + ); + } + if (!config.attachSearchResponse) { + throw new Error( + "no render target found, inline interface requires attachSearchResponse to be set" + ); + } + + const pipeline = config.pipeline + ? new Pipeline( + config.project, + config.collection, + config.pipeline, + undefined, + config.disableGA ? [] : undefined + ) + : null; + const values = config.pipeline ? new Values() : null; + const instantPipeline = config.instantPipeline + ? new Pipeline( + config.project, + config.collection, + config.instantPipeline, + pipeline ? new NoTracking() : undefined, + config.disableGA || pipeline ? [] : undefined + ) + : null; + const instantValues = config.instantPipeline ? new Values() : null; + + if (pipeline) connectPubSub(pub, sub, "pipeline", pipeline, values); + if (instantPipeline) + connectPubSub(pub, sub, "instantPipeline", instantPipeline, instantValues); + + const tabsFilter = setUpTabsFilters( + config, + pipeline || instantPipeline, + values || instantValues + ); + + if (values && values.get().q && instantValues) { + instantValues.set({ q: values.get().q }); + } + + ReactDOM.render( + , + config.attachSearchBox + ); + ReactDOM.render( + , + config.attachSearchResponse + ); +}; + +const initOverlay = (config, pub, sub) => { + if (!config.pipeline && !config.instantPipeline) { + throw new Error( + "no pipeline found, overlay interface requires at least 1 pipeline" + ); + } + + const pipeline = config.pipeline + ? new Pipeline( + config.project, + config.collection, + config.pipeline, + undefined, + config.disableGA ? [] : undefined + ) + : null; + const values = config.pipeline ? new Values() : null; + const instantPipeline = config.instantPipeline + ? new Pipeline( + config.project, + config.collection, + config.instantPipeline, + pipeline ? new NoTracking() : undefined, + config.disableGA || pipeline ? [] : undefined + ) + : null; + const instantValues = config.instantPipeline ? new Values() : null; + + if (pipeline) connectPubSub(pub, sub, "pipeline", pipeline, values); + if (instantPipeline) + connectPubSub(pub, sub, "instantPipeline", instantPipeline, instantValues); + + const tabsFilter = setUpTabsFilters( + config, + pipeline || instantPipeline, + values || instantValues + ); + + if (values && values.get().q && instantValues) { + instantValues.set({ q: values.get().q }); + } + + const setOverlayControls = controls => { + const show = () => { + document.getElementsByTagName("body")[0].style.overflow = "hidden"; + controls.show(); + }; + const hide = () => { + document.getElementsByTagName("body")[0].style.overflow = ""; + if (pipeline) { + values.set({ q: undefined, "q.override": undefined }); + pipeline.clearResponse(values.get()); + } + if (instantPipeline) { + instantValues.set({ q: undefined, "q.override": undefined }); + instantPipeline.clearResponse(instantValues.get()); + } + if (config.tabFilters && config.tabFilters.defaultTab) { + disableTabFacetSearch = true; + tabsFilter.set(config.tabFilters.defaultTab); + disableTabFacetSearch = false; + } + controls.hide(); + }; + sub(integrationEvents.overlayShow, show); + sub(integrationEvents.overlayHide, hide); + return { show, hide }; + }; + + // Create a container to render the overlay into + const overlayContainer = document.createElement("div"); + overlayContainer.id = "sj-overlay-holder"; + document.body.appendChild(overlayContainer); + + // Set up global overlay values + document.addEventListener("keydown", e => { + if (e.keyCode === ESCAPE_KEY_CODE) { + pub(integrationEvents.overlayHide); + } + }); + + ReactDOM.render( + , + overlayContainer + ); + + if ((values || instantValues).get().q) { + pub(integrationEvents.overlayShow); + } +}; + const initialise = () => { if (!window.sajari) { throw new Error("window.sajari not found, needed for website-search"); @@ -294,18 +450,63 @@ const initialise = () => { }; let configured = false; - const config = config => { - if (configured) { - throw new Error("website search interface can only be configured once"); + + const checkConfig = config => { + if (!config.project) { + throw new Error("'project' not set in config"); + } + if (!config.collection) { + throw new Error("'collection' not set in config"); } if (!config) { throw new Error("no config provided"); } + if (configured) { + throw new Error("website search interface can only be configured once"); + } + }; + + const createSearchbox = config => { + checkConfig(config); + initSearchbox(config, pub, sub); + configured = true; + }; + + const createInline = config => { + checkConfig(config); + initInline(config, pub, sub); + configured = true; + }; + + const createOverlay = config => { + checkConfig(config); + initOverlay(config, pub, sub); configured = true; - return initInterface(config, pub, sub); }; - const methods = { config, pub, sub }; + const createDynamicContent = config => { + checkConfig(config); + initDynamicContent(config, pub, sub); + configured = true; + }; + + const methods = { + pub, + sub, + "search-box": createSearchbox, + inline: createInline, + overlay: createOverlay, + "dynamic-content": createDynamicContent + }; + + for (let i = 0; i < s.arr.length; i++) { + if (typeof s.arr[i][0] === "object") { + if (!s.arr[i][0].mode) { + throw new Error("mode not found in config object"); + } + s.arr[i] = [s.arr[i][0].mode, s.arr[i][0]]; + } + } const errors = flush(s, methods); if (errors.length > 0) { diff --git a/src/styles.css b/src/styles.css index b44a23f..41f4f91 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,33 +1,294 @@ -.sj-content-block-result { - margin-top: 0px; - width: 32%; - padding: 0.5%; - float: left; - margin: 0 auto; +/** + * Search box + */ + +.sj-search-holder-outer { + padding: 0.9em 0px; + position: relative; + min-height: 72px; + box-sizing: border-box; +} + +.sj-overlay .sj-search-holder-outer { + margin-right: 34px; +} + +.sj-search-holder-inner { + position: absolute; + width: 100%; +} + +.sj-search-bar-input-common { + font-size: 20px; + padding: 0.4em; + outline: none; + letter-spacing: 0.6px; + line-height: 28px; + text-rendering: optimizeLegibility; + width: 100%; + box-shadow: 0 0 0 1px #ddd; + border-radius: 3px; + border: 0; + box-sizing: border-box; +} + +.sj-search-bar-completion { + color: #bebebe; +} + +.sj-search-bar-input { + position: absolute; + background: transparent; + color: #666; + top: 0px; + left: 0px; } -.sj-image { +.sj-search-icon { + display: none; +} + +/** + * Autocomplete override + */ + +.sj-result-summary-autocomplete-override { display: block; - margin: 0 auto; - width: auto; - max-width: 100%; - height: 100px; + padding-top: 16px; + font-size: 1.2em; +} + +.sj-result-summary-autocomplete-override > a { + color: #1a0dab; +} + +/** + * Suggestions + */ + +.sj-autocomplete-dropdown { + position: relative; +} + +.sj-suggestions { + border: 1px solid #ddd; + cursor: pointer; +} + +.sj-suggestion { + font-size: 18px; + padding: 8px 8px; + background-color: #fff; + color: #666; +} + +.sj-suggestion strong { + font-weight: 600; + color: #333; +} + +.sj-suggestion.sj-suggestion-selected, +.sj-suggestion:hover { + background-color: #ddd; +} + +/** + * Results + */ + +.sj-result-summary { + padding-bottom: 1.5em; + font-size: 16px; + color: #aaa; +} + +.sj-result { + clear: both; +} + +.sj-result-list > * { + margin-top: 1.5em; } -.sj-image-text { +.sj-result-list > :first-child { + margin-top: 0; +} + +.sj-result-title { + margin-bottom: 0; + margin-top: 0; + font-size: 16px; + line-height: 24px; + white-space: nowrap; + overflow: hidden; text-overflow: ellipsis; +} + +.sj-result-title a { + text-decoration: none; + font-weight: 400; + font-size: 20px; + color: #333; + line-height: 21.6px; +} + +.sj-result-title a:hover { + text-decoration: underline; +} + +.sj-result-description { + color: #545454; + font-size: 15px; + line-height: 22px; + overflow-wrap: break-word; + margin-top: 2px; + margin-bottom: 4px; +} + +.sj-result-url { + font-size: 13px; + line-height: 18.2px; + margin: 0; + color: #a2a2a2; + white-space: nowrap; overflow: hidden; + text-overflow: ellipsis; +} + +.sj-result-url a { + text-decoration: none; + color: #a2a2a2; +} + +.sj-result-image-container { + float: left; + width: 100px; +} + +.sj-result-image-container img { + max-height: 90px; + max-width: 90px; +} + +/** + * Paginator + */ + +.sj-paginator { + margin: 1em 0; + text-align: center; +} + +.sj-paginator > div { + display: inline; + padding: 10px; + color: #777; + font-weight: bold; + cursor: pointer; + user-select: none; +} + +.sj-paginator > div.current { + color: #333; +} + +.sj-paginator > div.disabled { + color: #aaa; +} + +/** + * Tabs + */ + +.sj-tabs-container { + border-bottom: 1px solid #ebebeb; + color: #777; + width: 100%; + margin-bottom: 1em; +} + +.sj-tabs { + overflow: auto; white-space: nowrap; } -@media (max-width: 768px) { - .sj-content-block-result { - width: 49%; +.sj-tab { + display: inline-block; + font-size: 16px; + cursor: pointer; + margin: 0; + padding: .9em; + user-select: none; +} + +.sj-tab-active { + color: #333; + border-bottom: 3px solid #333; +} + +/** + * Overlay + */ +@keyframes sj-overlay-rolldown { + 0% { + opacity: 0; + height: 20%; } + 100% { + opacity: 1; + height: 100%; + } +} + +.sj-overlay { + background-color: white; + position: fixed; + top: 0; + bottom: 0; + left: 0; + overflow-x: hidden; + overflow-y: auto; + width: 100%; + height: 100%; + z-index: 1000000; + animation-name: sj-overlay-rolldown; + animation-duration: 0.4s; +} + +.sj-overlay-search { + padding: 0px 30px; } -@media (max-width: 480px) { - .sj-content-block-result { - width: 99%; +.sj-overlay-close { + position: absolute; + top: 0; + right: 20px; + z-index: 1; + color: #aaa; + text-align: center; + cursor: pointer; +} + +.sj-overlay-close:hover { + color: #000; +} + +.sj-overlay-close .sj-close { + font-size: 40px; + line-height: 30px; + padding-top: 10px; +} + +.sj-overlay-close .sj-esc { + font-size: 12px; +} + +@media (max-width: 768px) { + .sj-overlay-search { + padding: 0px 10px; + } + + .sj-overlay .sj-search-holder-outer { + margin-right: 54px; } }