diff --git a/modules/search/class.jetpack-search.php b/modules/search/class.jetpack-search.php index dc87b16a53996..396bd4a62d32d 100644 --- a/modules/search/class.jetpack-search.php +++ b/modules/search/class.jetpack-search.php @@ -204,10 +204,24 @@ public function load_assets() { $script_version = self::get_asset_version( $script_relative_path ); $script_path = plugins_url( $script_relative_path, JETPACK__PLUGIN_FILE ); wp_enqueue_script( 'jetpack-instant-search', $script_path, array(), $script_version, true ); - $_blog_id = Jetpack::get_option( 'id' ); + + $filters = Jetpack_Search_Helpers::get_filters_from_widgets(); + $widgets = array(); + foreach( $filters as $key => $filter ) { + if ( ! isset( $widgets[$filter[ 'widget_id' ]] ) ) { + $widgets[$filter[ 'widget_id' ]][ 'filters' ] = array(); + $widgets[$filter[ 'widget_id' ]][ 'widget_id' ] = $filter[ 'widget_id' ]; + } + $new_filter = $filter; + $new_filter[ 'filter_id' ] = $key; + $widgets[$filter[ 'widget_id' ]][ 'filters' ][] = $new_filter; + } + // This is probably a temporary filter for testing the prototype. $options = array( - 'siteId' => $_blog_id, + 'postTypes' => get_post_types(), + 'siteId' => Jetpack::get_option( 'id' ), + 'widgets' => array_values( $widgets ), ); /** * Customize Instant Search Options. @@ -221,9 +235,7 @@ public function load_assets() { $options = apply_filters( 'jetpack_instant_search_options', $options ); wp_localize_script( - 'jetpack-instant-search', - 'jetpack_instant_search_options', - $options + 'jetpack-instant-search', 'JetpackInstantSearchOptions', $options ); } diff --git a/modules/search/instant-search/components/api.js b/modules/search/instant-search/components/api.js deleted file mode 100644 index 0cbbbcd5cefb1..0000000000000 --- a/modules/search/instant-search/components/api.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * External dependencies - */ -import fetch from 'unfetch'; - -const FIELDS = [ - 'author', - 'comment_count', - 'date', - 'excerpt_html', - 'gravatar_url', - 'permalink.url.raw', - 'title_html', -]; - -function stringifyArray( fieldName, array ) { - return array.map( ( element, index ) => `${ fieldName }[${ index }]=${ element }` ).join( '&' ); -} - -function getAPIUrl( siteId, query ) { - return `https://public-api.wordpress.com/rest/v1.3/sites/${ siteId }/search?query=${ encodeURIComponent( - query - ) }&${ stringifyArray( 'fields', FIELDS ) }`; -} - -export function search( siteId, query ) { - return fetch( getAPIUrl( siteId, query ) ); -} diff --git a/modules/search/instant-search/components/gridicon/index.jsx b/modules/search/instant-search/components/gridicon/index.jsx new file mode 100644 index 0000000000000..a0419136ffe58 --- /dev/null +++ b/modules/search/instant-search/components/gridicon/index.jsx @@ -0,0 +1,380 @@ +/** @jsx h **/ + +/* !!! +This is a fork of the Jetpack Gridicon code: + https://github.com/Automattic/jetpack/blob/f8078c2cd12ac508334da2fb08e37a92cf283c14/_inc/client/components/gridicon/index.jsx + +It has been modified to work with Preact, and only includes the icons that we need. +!!! */ + +/** + * External dependencies + */ +import { h, Component } from 'preact'; +import './style.scss'; + +class Gridicon extends Component { + needsOffset( icon, size ) { + const iconNeedsOffset = [ + 'gridicons-add-outline', + 'gridicons-add', + 'gridicons-align-image-center', + 'gridicons-align-image-left', + 'gridicons-align-image-none', + 'gridicons-align-image-right', + 'gridicons-attachment', + 'gridicons-backspace', + 'gridicons-bold', + 'gridicons-bookmark-outline', + 'gridicons-bookmark', + 'gridicons-calendar', + 'gridicons-cart', + 'gridicons-create', + 'gridicons-custom-post-type', + 'gridicons-external', + 'gridicons-folder', + 'gridicons-heading', + 'gridicons-help-outline', + 'gridicons-help', + 'gridicons-history', + 'gridicons-info-outline', + 'gridicons-info', + 'gridicons-italic', + 'gridicons-layout-blocks', + 'gridicons-link-break', + 'gridicons-link', + 'gridicons-list-checkmark', + 'gridicons-list-ordered', + 'gridicons-list-unordered', + 'gridicons-menus', + 'gridicons-minus', + 'gridicons-my-sites', + 'gridicons-notice-outline', + 'gridicons-notice', + 'gridicons-plans', + 'gridicons-plus-small', + 'gridicons-plus', + 'gridicons-popout', + 'gridicons-posts', + 'gridicons-scheduled', + 'gridicons-share-ios', + 'gridicons-star-outline', + 'gridicons-star', + 'gridicons-stats', + 'gridicons-status', + 'gridicons-thumbs-up', + 'gridicons-textcolor', + 'gridicons-time', + 'gridicons-trophy', + 'gridicons-user-circle', + ]; + + if ( iconNeedsOffset.indexOf( icon ) >= 0 ) { + return size % 18 === 0; + } + return false; + } + + needsOffsetX( icon, size ) { + const iconNeedsOffsetX = [ + 'gridicons-arrow-down', + 'gridicons-arrow-up', + 'gridicons-comment', + 'gridicons-clear-formatting', + 'gridicons-flag', + 'gridicons-menu', + 'gridicons-reader', + 'gridicons-strikethrough', + ]; + + if ( iconNeedsOffsetX.indexOf( icon ) >= 0 ) { + return size % 18 === 0; + } + return false; + } + + needsOffsetY( icon, size ) { + const iconNeedsOffsetY = [ + 'gridicons-align-center', + 'gridicons-align-justify', + 'gridicons-align-left', + 'gridicons-align-right', + 'gridicons-arrow-left', + 'gridicons-arrow-right', + 'gridicons-house', + 'gridicons-indent-left', + 'gridicons-indent-right', + 'gridicons-minus-small', + 'gridicons-print', + 'gridicons-sign-out', + 'gridicons-stats-alt', + 'gridicons-trash', + 'gridicons-underline', + 'gridicons-video-camera', + ]; + + if ( iconNeedsOffsetY.indexOf( icon ) >= 0 ) { + return size % 18 === 0; + } + return false; + } + + renderIcon( icon ) { + switch ( icon ) { + default: + return null; + case 'gridicons-attachment': + return ( + + + + ); + case 'gridicons-audio': + return ( + + + + ); + case 'gridicons-calendar': + return ( + + + + ); + case 'gridicons-camera': + return ( + + + + ); + case 'gridicons-cart': + return ( + + + + ); + case 'gridicons-chevron-down': + return ( + + + + ); + case 'gridicons-chevron-left': + return ( + + + + ); + case 'gridicons-chevron-right': + return ( + + + + ); + case 'gridicons-chevron-up': + return ( + + + + ); + case 'gridicons-code': + return ( + + + + ); + case 'gridicons-comment': + return ( + + + + ); + case 'gridicons-cross-small': + return ( + + + + ); + case 'gridicons-cross': + return ( + + + + ); + case 'gridicons-dropdown': + return ( + + + + ); + case 'gridicons-external': + return ( + + + + ); + case 'gridicons-folder-multiple': + return ( + + + + ); + case 'gridicons-folder': + return ( + + + + ); + case 'gridicons-image-multiple': + return ( + + + + ); + case 'gridicons-image': + return ( + + + + ); + case 'gridicons-location': + return ( + + + + ); + case 'gridicons-mention': + return ( + + + + ); + case 'gridicons-my-sites': + return ( + + + + ); + case 'gridicons-pages': + return ( + + + + ); + case 'gridicons-plus-small': + return ( + + + + ); + case 'gridicons-plus': + return ( + + + + ); + case 'gridicons-reader': + return ( + + + + ); + case 'gridicons-refresh': + return ( + + + + ); + case 'gridicons-search': + return ( + + + + ); + case 'gridicons-star-outline': + return ( + + + + ); + case 'gridicons-star': + return ( + + + + ); + case 'gridicons-stats-alt': + return ( + + + + ); + case 'gridicons-stats': + return ( + + + + ); + case 'gridicons-tag': + return ( + + + + ); + case 'gridicons-user-circle': + return ( + + + + ); + case 'gridicons-user': + return ( + + + + ); + case 'gridicons-video': + return ( + + + + ); + } + } + + render() { + const { size = 24, class_name = '' } = this.props; + const icon = 'gridicons-' + this.props.icon, + needsOffset = this.needsOffset( icon, size ), + needsOffsetX = this.needsOffsetX( icon, size ), + needsOffsetY = this.needsOffsetY( icon, size ); + + let iconClass = [ 'gridicon', icon, class_name ]; + + if ( needsOffset ) { + iconClass.push( 'needs-offset' ); + } + if ( needsOffsetX ) { + iconClass.push( 'needs-offset-x' ); + } + if ( needsOffsetY ) { + iconClass.push( 'needs-offset-y' ); + } + iconClass = iconClass.join( ' ' ); + + return ( + + { this.renderIcon( icon ) } + + ); + } +} + +export default Gridicon; diff --git a/modules/search/instant-search/components/gridicon/style.scss b/modules/search/instant-search/components/gridicon/style.scss new file mode 100644 index 0000000000000..196bcc1a30875 --- /dev/null +++ b/modules/search/instant-search/components/gridicon/style.scss @@ -0,0 +1,15 @@ +.gridicon { + fill: currentColor; + + &.needs-offset g { + transform: translate( 1px, 1px ); /* translates to .5px because it's in a child element */ + } + + &.needs-offset-x g { + transform: translate( 1px, 0 ); /* only nudges horizontally */ + } + + &.needs-offset-y g { + transform: translate( 0, 1px ); /* only nudges vertically */ + } +} diff --git a/modules/search/instant-search/components/search-filter-dates.jsx b/modules/search/instant-search/components/search-filter-dates.jsx new file mode 100644 index 0000000000000..2d8e3340fcaba --- /dev/null +++ b/modules/search/instant-search/components/search-filter-dates.jsx @@ -0,0 +1,42 @@ +/** @jsx h */ + +/** + * External dependencies + */ +import { h, Component } from 'preact'; +import strip from 'strip'; + +export default class SearchFilterDates extends Component { + render() { + return ( +
+

{ this.props.filter.name }

+ +
+ ); + } +} diff --git a/modules/search/instant-search/components/search-filter-post-types.jsx b/modules/search/instant-search/components/search-filter-post-types.jsx new file mode 100644 index 0000000000000..3ff2751426221 --- /dev/null +++ b/modules/search/instant-search/components/search-filter-post-types.jsx @@ -0,0 +1,38 @@ +/** @jsx h */ + +/** + * External dependencies + */ +import { h, Component } from 'preact'; +import strip from 'strip'; + +export default class SearchFilterPostTypes extends Component { + renderPostType = ( { key, doc_count: count } ) => { + const name = this.props.postTypes[ key ]; + return ( +
+ + +
+ ); + }; + render() { + return ( +
+

{ this.props.filter.name }

+ +
+ ); + } +} diff --git a/modules/search/instant-search/components/search-filter-taxonomies.jsx b/modules/search/instant-search/components/search-filter-taxonomies.jsx new file mode 100644 index 0000000000000..fbd93a7fcfdba --- /dev/null +++ b/modules/search/instant-search/components/search-filter-taxonomies.jsx @@ -0,0 +1,34 @@ +/** @jsx h */ + +/** + * External dependencies + */ +import { h, Component } from 'preact'; +import strip from 'strip'; + +export default class SearchFilterTaxonomies extends Component { + render() { + return ( +
+

{ this.props.filter.name }

+ +
+ ); + } +} diff --git a/modules/search/instant-search/components/search-filters-widget.jsx b/modules/search/instant-search/components/search-filters-widget.jsx new file mode 100644 index 0000000000000..efbefd835cb9a --- /dev/null +++ b/modules/search/instant-search/components/search-filters-widget.jsx @@ -0,0 +1,56 @@ +/** @jsx h */ + +/** + * External dependencies + */ +import { h, Component } from 'preact'; +// NOTE: We only import the debounce package here for to reduced bundle size. +// Do not import the entire lodash library! +// eslint-disable-next-line lodash/import-scope +import get from 'lodash/get'; + +/** + * Internal dependencies + */ +import SearchFilterDates from './search-filter-dates'; +import SearchFilterTaxonomies from './search-filter-taxonomies'; +import SearchFilterPostTypes from './search-filter-post-types'; + +export default class SearchFiltersWidget extends Component { + renderFilterComponent = ( { filter, results } ) => { + switch ( filter.type ) { + case 'date_histogram': + return results && ; + case 'taxonomy': + return results && ; + case 'post_type': + return ( + results && ( + + ) + ); + } + }; + + render() { + const aggregations = get( this.props.results, 'aggregations' ); + return ( +
+ { get( this.props.widget, 'filters' ) + .map( filter => + aggregations ? { filter, results: aggregations[ filter.filter_id ] } : null + ) + .filter( data => !! data ) + .filter( + ( { results } ) => + !! results && Array.isArray( results.buckets ) && results.buckets.length > 0 + ) + .map( this.renderFilterComponent ) } +
+ ); + } +} diff --git a/modules/search/instant-search/components/search-filters-widget.scss b/modules/search/instant-search/components/search-filters-widget.scss new file mode 100644 index 0000000000000..d836499e7453a --- /dev/null +++ b/modules/search/instant-search/components/search-filters-widget.scss @@ -0,0 +1,5 @@ +.jetpack-search-filters-widget__filter-list { + label { + display: inline-block; + } +} diff --git a/modules/search/instant-search/components/search-result-minimal.jsx b/modules/search/instant-search/components/search-result-minimal.jsx new file mode 100644 index 0000000000000..f03f54135af28 --- /dev/null +++ b/modules/search/instant-search/components/search-result-minimal.jsx @@ -0,0 +1,105 @@ +/** @jsx h */ + +/** + * External dependencies + */ +import { h, Component } from 'preact'; +import strip from 'strip'; + +/** + * Internal dependencies + */ +import Gridicon from './gridicon'; + +class SearchResultMinimal extends Component { + render() { + const { result_type, fields, highlight } = this.props.result; + const IconSize = 18; + if ( result_type !== 'post' ) { + return null; + } + const url = new URL( 'http://' + fields[ 'permalink.url.raw' ] ); + const path = url.pathname; + const no_content = ! highlight.content || highlight.content[ 0 ] === ''; + + let tags = fields[ 'tag.name.default' ]; + if ( ! tags ) { + tags = []; + } + if ( ! Array.isArray( tags ) ) { + tags = [ tags ]; + } + + let cats = fields[ 'category.name.default' ]; + if ( ! cats ) { + cats = []; + } + if ( ! Array.isArray( cats ) ) { + cats = [ cats ]; + } + + return ( +
+

+ +

+ + { strip( fields.date ).split( ' ' )[ 0 ] } + + + { no_content && ( +
+
{ path }
+
+ { tags.map( tag => ( + + + { tag } + + ) ) } +
+
+ { cats.map( cat => ( + + + { cat } + + ) ) } +
+
+ ) } + { ! no_content && ( +
+ ) } + + { highlight.comments && ( +
+ + +
+ ) } +
+ ); + } +} + +export default SearchResultMinimal; diff --git a/modules/search/instant-search/components/search-result-minimal.scss b/modules/search/instant-search/components/search-result-minimal.scss new file mode 100644 index 0000000000000..0fb9d8e935beb --- /dev/null +++ b/modules/search/instant-search/components/search-result-minimal.scss @@ -0,0 +1,39 @@ +.jetpack-instant-search__result-minimal { + padding: 0.125em 0; + margin: 1em 0; + position: relative; +} +.jetpack-instant-search__result-minimal .gridicon { + margin-right: 5px; +} + +.jetpack-instant-search__result-minimal h3 { + overflow: hidden; +} + +.jetpack-instant-search__result-minimal-date { + margin: 0.5em 0; + display: flex; + float: right; + position: absolute; + top: 0px; + right: 0px; + font-size: 0.85em; +} + +.jetpack-instant-search__result-minimal-tag span, +.jetpack-instant-search__result-minimal-cat span { + margin-right: 0.5em; +} + +.jetpack-instant-search__result-minimal-tag, +.jetpack-instant-search__result-minimal-cat, +.jetpack-instant-search__result-minimal-comment { + padding-left: 10px; +} + +.jetpack-instant-search__result-minimal-title b, +.jetpack-instant-search__result-minimal-content b, +.jetpack-instant-search__result-minimal-comment-span b { + text-decoration: underline; +} diff --git a/modules/search/instant-search/components/search-result.jsx b/modules/search/instant-search/components/search-result.jsx deleted file mode 100644 index 26df807f00ce9..0000000000000 --- a/modules/search/instant-search/components/search-result.jsx +++ /dev/null @@ -1,42 +0,0 @@ -/** @jsx h */ - -/** - * External dependencies - */ -import { sprintf, _n } from '@wordpress/i18n'; -import { h, Component } from 'preact'; -import strip from 'strip'; - -class SearchResult extends Component { - render() { - return ( -
- - { strip( this.props.result.fields.title_html ) || 'Unknown Title' } - {' '} -
- { strip( this.props.result.fields.author ) }{' '} - - { strip( this.props.result.fields.date ).split( ' ' )[ 0 ] } - -
-
- { strip( this.props.result.fields.excerpt_html ) } -
-
- { sprintf( - _n( '%d comment', '%d comments', this.props.result.fields.comment_count, 'jetpack' ), - this.props.result.fields.comment_count - ) } -
-
- ); - } -} - -export default SearchResult; diff --git a/modules/search/instant-search/components/search-result.scss b/modules/search/instant-search/components/search-result.scss deleted file mode 100644 index 5bb1b9926d472..0000000000000 --- a/modules/search/instant-search/components/search-result.scss +++ /dev/null @@ -1,28 +0,0 @@ -@import '../../../../_inc/client/scss/variables/colors'; - -.jetpack-instant-search__result { - padding: 0.125em 0; - margin: 1em 0; - a { - box-shadow: inset 0 0 0 rgba( black, 0 ), 0 1px 0 rgba( black, 1 ); - transition: color 80ms ease-in, box-shadow 130ms ease-in-out; - } - a:hover, - a:focus { - box-shadow: inset 0 0 0 rgba( black, 0 ), 0 3px 0 rgba( black, 1 ); - } -} - -.jetpack-instant-search__result-author-and-date { - margin: 0.5em 0; - display: flex; - justify-content: space-between; -} - -.jetpack-instant-search__result-date { - color: $gray; -} - -.jetpack-instant-search__result-excerpt { - color: $gray; -} diff --git a/modules/search/instant-search/components/search-results.jsx b/modules/search/instant-search/components/search-results.jsx index 6c631c27444b1..55340d2086d29 100644 --- a/modules/search/instant-search/components/search-results.jsx +++ b/modules/search/instant-search/components/search-results.jsx @@ -9,17 +9,50 @@ import { h, Component } from 'preact'; /** * Internal dependencies */ -import SearchResult from './search-result'; +import SearchResultMinimal from './search-result-minimal'; class SearchResults extends Component { + render_result( result ) { + switch ( this.props.resultFormat ) { + case 'engagement': + case 'product': + case 'minimal': + default: + return ; + } + } + render() { - const { results = [], query } = this.props; + const { results = [], query, total = 0, corrected_query = false } = this.props; + if ( query === '' ) { + return
; + } + if ( total === 0 ) { + return ( +
+
+

{ sprintf( __( 'No Results.' ), query ) }

+
+
+ ); + } + return (
-

{ sprintf( __( 'You are searching for: "%s"' ), query ) }

- { results.map( result => ( - - ) ) } +

+ { corrected_query !== false + ? sprintf( __( 'Showing results for "%s"' ), corrected_query ) + : sprintf( __( 'Results for "%s"' ), query ) } +

+ { corrected_query !== false && ( +

+ { sprintf( __( 'No results for "%s"' ), query ) } +

+ ) } + + { sprintf( __( '%d Results' ), total ) } + + { results.map( result => this.render_result( result ) ) }
); } diff --git a/modules/search/instant-search/components/search-results.scss b/modules/search/instant-search/components/search-results.scss new file mode 100644 index 0000000000000..0a093ae4fac6d --- /dev/null +++ b/modules/search/instant-search/components/search-results.scss @@ -0,0 +1,23 @@ +.jetpack-instant-search__search-results { + padding: 0.125em 0; + margin: 1em 0; + position: relative; +} + +.jetpack-instant-search__search-results-count { + position: absolute; + top: 0; + right: 0; +} + +.jetpack-instant-search__search-results-real-query { + font-size: 1.5em; + text-decoration: bold; +} + +.jetpack-instant-search__search-results-unused-query, +.jetpack-instant-search__search-results-real-query { + overflow: hidden; + margin: 0; + padding: 0; +} diff --git a/modules/search/instant-search/components/search-widget.jsx b/modules/search/instant-search/components/search-widget.jsx index 07f6140e9d43d..4fb965c6d6dd0 100644 --- a/modules/search/instant-search/components/search-widget.jsx +++ b/modules/search/instant-search/components/search-widget.jsx @@ -3,7 +3,7 @@ /** * External dependencies */ -import { h, Component } from 'preact'; +import Preact, { h, Component } from 'preact'; import Portal from 'preact-portal'; // NOTE: We only import the debounce package here for to reduced bundle size. // Do not import the entire lodash library! @@ -14,27 +14,39 @@ import debounce from 'lodash/debounce'; * Internal dependencies */ import SearchResults from './search-results'; -import { search } from './api'; +import SearchFiltersWidget from './search-filters-widget'; +import { search, buildFilterAggregations } from '../lib/api'; import { setSearchQuery } from '../lib/query-string'; +import { removeChildren, hideSearchHeader } from '../lib/dom'; class SearchApp extends Component { constructor() { super( ...arguments ); + this.input = Preact.createRef(); this.requestId = 0; + this.props.resultFormat = 'minimal'; + this.props.aggregations = buildFilterAggregations( this.props.options.widgets ); + this.props.widgets = this.props.options.widgets ? this.props.options.widgets : []; this.state = { query: this.props.initialValue, - results: [], + results: {}, }; this.getResults = debounce( this.getResults, 500 ); this.getResults( this.props.initialValue ); } + componentDidMount() { if ( this.props.grabFocus ) { - this.input.focus(); + this.input.current.focus(); } + + hideSearchHeader(); + removeChildren( document.querySelector( 'main' ) ); + this.props.widgets.forEach( function( widget ) { + removeChildren( document.getElementById( widget.widget_id ) ); + } ); } - bindInput = input => ( this.input = input ); onChangeQuery = event => { const query = event.target.value; this.setState( { query } ); @@ -47,7 +59,13 @@ class SearchApp extends Component { this.requestId++; const requestId = this.requestId; - search( this.props.siteId, query ) + search( + this.props.options.siteId, + query, + this.props.aggregations, + {}, + this.props.options.resultFormat + ) .then( response => response.json() ) .then( json => { if ( this.requestId === requestId ) { @@ -62,19 +80,44 @@ class SearchApp extends Component { render() { const { query, results } = this.state; return ( -
-

- -

+ + { this.props.widgets.map( ( widget, index ) => ( + +
+
+ { /* TODO: Add support for preserving label text */ } + + +
+
+ +
+ + ) ) } + - + -
+
); } } diff --git a/modules/search/instant-search/index.jsx b/modules/search/instant-search/index.jsx index 65b8274f3be66..7a12766b734b6 100644 --- a/modules/search/instant-search/index.jsx +++ b/modules/search/instant-search/index.jsx @@ -11,37 +11,19 @@ import { h, render } from 'preact'; import SearchWidget from './components/search-widget'; import { getSearchQuery } from './lib/query-string'; -function removeChildren( htmlElement ) { - while ( htmlElement.lastChild ) { - htmlElement.removeChild( htmlElement.lastChild ); - } -} - -const hideSearchHeader = () => { - const titleElements = document.getElementById( 'content' ).getElementsByClassName( 'page-title' ); - if ( titleElements.length > 0 ) { - titleElements[ 0 ].style.display = 'none'; - } -}; - -const injectSearchWidget = ( initialValue, target, siteId, grabFocus ) => { +const injectSearchWidget = ( initialValue, grabFocus ) => { render( - , - target + , + document.body ); }; document.addEventListener( 'DOMContentLoaded', function() { - //This var is provided by wp_localize_script() so we have limited control - const options = jetpack_instant_search_options; // eslint-disable-line no-undef - - if ( 'siteId' in options && document.body && document.body.classList.contains( 'search' ) ) { - const widget = document.querySelector( '.widget_search' ); - if ( !! widget ) { - removeChildren( widget ); - removeChildren( document.querySelector( 'main' ) ); - hideSearchHeader(); - injectSearchWidget( getSearchQuery(), widget, options.siteId ); - } + if ( !! window.JetpackInstantSearchOptions && 'siteId' in window.JetpackInstantSearchOptions ) { + injectSearchWidget( getSearchQuery() ); } } ); diff --git a/modules/search/instant-search/instant-search.scss b/modules/search/instant-search/instant-search.scss index c42b08a53d8a0..8bbacfb1c1b23 100644 --- a/modules/search/instant-search/instant-search.scss +++ b/modules/search/instant-search/instant-search.scss @@ -1 +1,5 @@ -@import './components/search-result.scss'; +@import './components/search-results.scss'; +@import './components/search-filters-widget.scss'; +@import './components/search-result-minimal.scss'; +//@import './components/search-result-engagement.scss'; +//@import './components/search-result-product.scss'; diff --git a/modules/search/instant-search/lib/api.js b/modules/search/instant-search/lib/api.js new file mode 100644 index 0000000000000..0cef521b28608 --- /dev/null +++ b/modules/search/instant-search/lib/api.js @@ -0,0 +1,65 @@ +/** + * External dependencies + */ +import fetch from 'unfetch'; +import { encode } from 'qss'; +import { flatten } from 'q-flat'; + +export function buildFilterAggregations( widgets = [] ) { + const aggregation = {}; + widgets.forEach( ( { filters: widgetFilters } ) => + widgetFilters.forEach( filter => { + switch ( filter.type ) { + case 'date_histogram': { + const field = filter.field === 'post_date_gmt' ? 'date_gmt' : 'date'; + aggregation[ filter.filter_id ] = { + date_histogram: { field, interval: filter.interval }, + }; + break; + } + case 'taxonomy': { + let field = `taxonomy.${ filter.taxonomy }.slug`; + if ( filter.taxonomy === 'post_tag' ) { + field = 'tag.slug'; + } else if ( filter.type === 'category' ) { + field = 'category.slug'; + } + aggregation[ filter.filter_id ] = { terms: { field, size: filter.count } }; + break; + } + case 'post_type': { + aggregation[ filter.filter_id ] = { terms: { field: filter.type, size: filter.count } }; + break; + } + } + } ) + ); + return aggregation; +} + +export function search( siteId, query, aggregations, filter, resultFormat ) { + let fields = []; + let highlight_fields = []; + switch ( resultFormat ) { + case 'engagement': + case 'product': + case 'minimal': + default: + highlight_fields = [ 'title', 'content', 'comments' ]; + fields = [ 'date', 'permalink.url.raw', 'tag.name.default', 'category.name.default' ]; + } + + const queryString = encode( + flatten( { + aggregations, + fields, + highlight_fields, + filter, + query: encodeURIComponent( query ), + } ) + ); + + return fetch( + `https://public-api.wordpress.com/rest/v1.3/sites/${ siteId }/search?${ queryString }` + ); +} diff --git a/modules/search/instant-search/lib/dom.js b/modules/search/instant-search/lib/dom.js new file mode 100644 index 0000000000000..707f88d241c56 --- /dev/null +++ b/modules/search/instant-search/lib/dom.js @@ -0,0 +1,12 @@ +export function removeChildren( htmlElement ) { + while ( htmlElement.lastChild ) { + htmlElement.removeChild( htmlElement.lastChild ); + } +} + +export function hideSearchHeader() { + const title = document.querySelector( '#content .page-title' ); + if ( title ) { + title.style.display = 'none'; + } +} diff --git a/package.json b/package.json index 13af00d7bdd7f..425d035c7bff0 100644 --- a/package.json +++ b/package.json @@ -132,6 +132,7 @@ "preact": "8.4.2", "preact-portal": "1.1.3", "prop-types": "15.7.2", + "q-flat": "1.0.7", "qss": "2.0.3", "react-pure-render": "1.0.2", "react-redux": "6.0.1", diff --git a/webpack.config.js b/webpack.config.js index 3fac55f661935..7bf09ce9f594f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -85,8 +85,8 @@ module.exports = [ performance: isDevelopment ? {} : { - maxAssetSize: 30000, - maxEntrypointSize: 30000, + maxAssetSize: 35000, + maxEntrypointSize: 35000, hints: 'error', }, }, diff --git a/yarn.lock b/yarn.lock index 5c0387a172241..b2dc5ebcdb569 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10307,6 +10307,11 @@ puppeteer@1.19.0: rimraf "^2.6.1" ws "^6.1.0" +q-flat@1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/q-flat/-/q-flat-1.0.7.tgz#4cf81aede99d2a9acbafcf503af0d0184a850d3d" + integrity sha512-Ug+B6yajVE5HF7eAszOvAcYmQ+DbYaDcQlxYuW9RaAqwZTRZQq+lHMGqHlnaxKP7CfuGCpXQXOb4qymRYMkYEQ== + q@^1.1.2: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"