diff --git a/modules/search/instant-search/components/api.js b/modules/search/instant-search/components/api.js index 0cbbbcd5cefb1..701fb69ca9243 100644 --- a/modules/search/instant-search/components/api.js +++ b/modules/search/instant-search/components/api.js @@ -3,26 +3,73 @@ */ import fetch from 'unfetch'; -const FIELDS = [ - 'author', - 'comment_count', - 'date', - 'excerpt_html', - 'gravatar_url', - 'permalink.url.raw', - 'title_html', -]; +/** + * @preserve jquery-param (c) 2015 KNOWLEDGECODE | MIT + * @param {object} a - parameters to add + * @returns {string} url query string + * From https://github.com/knowledgecode/jquery-param + */ +function query_params( a ) { + //eslint-disable-next-line no-var + var s = []; + const add = function( k, v ) { + v = typeof v === 'function' ? v() : v; + //eslint-disable-next-line no-nested-ternary + v = v === null ? '' : v === undefined ? '' : v; + s[ s.length ] = encodeURIComponent( k ) + '=' + encodeURIComponent( v ); + }; + const buildParams = function( prefix, obj ) { + let i, len, key; -function stringifyArray( fieldName, array ) { - return array.map( ( element, index ) => `${ fieldName }[${ index }]=${ element }` ).join( '&' ); + if ( prefix ) { + if ( Array.isArray( obj ) ) { + for ( i = 0, len = obj.length; i < len; i++ ) { + buildParams( + prefix + '[' + ( typeof obj[ i ] === 'object' && obj[ i ] ? i : '' ) + ']', + obj[ i ] + ); + } + } else if ( String( obj ) === '[object Object]' ) { + for ( key in obj ) { + buildParams( prefix + '[' + key + ']', obj[ key ] ); + } + } else { + add( prefix, obj ); + } + } else if ( Array.isArray( obj ) ) { + for ( i = 0, len = obj.length; i < len; i++ ) { + add( obj[ i ].name, obj[ i ].value ); + } + } else { + for ( key in obj ) { + buildParams( key, obj[ key ] ); + } + } + return s; + }; + return buildParams( '', a ).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, aggs, 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' ]; + } -export function search( siteId, query ) { - return fetch( getAPIUrl( siteId, query ) ); + const obj = { + query: query, + fields: fields, + highlight_fields: highlight_fields, + aggregations: aggs, + filter: filter, + }; + return fetch( + `https://public-api.wordpress.com/rest/v1.3/sites/${ siteId }/search?${ query_params( obj ) }` + ); } 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-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..b3b85499b136d 100644 --- a/modules/search/instant-search/components/search-widget.jsx +++ b/modules/search/instant-search/components/search-widget.jsx @@ -21,6 +21,7 @@ class SearchApp extends Component { constructor() { super( ...arguments ); this.requestId = 0; + this.props.resultFormat = 'minimal'; this.state = { query: this.props.initialValue, results: [], @@ -47,7 +48,7 @@ class SearchApp extends Component { this.requestId++; const requestId = this.requestId; - search( this.props.siteId, query ) + search( this.props.options.siteId, query, {}, {}, this.props.options.resultFormat ) .then( response => response.json() ) .then( json => { if ( this.requestId === requestId ) { @@ -72,7 +73,11 @@ class SearchApp extends Component { />

- +
); diff --git a/modules/search/instant-search/index.jsx b/modules/search/instant-search/index.jsx index 65b8274f3be66..93cd0545889cc 100644 --- a/modules/search/instant-search/index.jsx +++ b/modules/search/instant-search/index.jsx @@ -24,9 +24,13 @@ const hideSearchHeader = () => { } }; -const injectSearchWidget = ( initialValue, target, siteId, grabFocus ) => { +const injectSearchWidget = ( initialValue, target, options, grabFocus ) => { render( - , + , target ); }; @@ -41,7 +45,7 @@ document.addEventListener( 'DOMContentLoaded', function() { removeChildren( widget ); removeChildren( document.querySelector( 'main' ) ); hideSearchHeader(); - injectSearchWidget( getSearchQuery(), widget, options.siteId ); + injectSearchWidget( getSearchQuery(), widget, options ); } } } ); diff --git a/modules/search/instant-search/instant-search.scss b/modules/search/instant-search/instant-search.scss index c42b08a53d8a0..2c20c43c8a253 100644 --- a/modules/search/instant-search/instant-search.scss +++ b/modules/search/instant-search/instant-search.scss @@ -1 +1,4 @@ -@import './components/search-result.scss'; +@import './components/search-results.scss'; +@import './components/search-result-minimal.scss'; +//@import './components/search-result-engagement.scss'; +//@import './components/search-result-product.scss';