woocommerce/plugins/woocommerce-blocks/assets/js/views/specific-select.jsx

378 lines
8.7 KiB
React
Raw Normal View History

2018-02-15 18:16:14 +00:00
const { __ } = wp.i18n;
2018-02-26 22:52:12 +00:00
const { Toolbar, withAPIData, Dropdown, Dashicon } = wp.components;
2018-02-22 18:48:34 +00:00
const { TransitionGroup, CSSTransition } = ReactTransitionGroup;
2018-02-15 18:16:14 +00:00
2018-03-02 19:18:42 +00:00
/**
* Product data cache.
* Reduces the number of API calls and makes UI smoother and faster.
*/
const PRODUCT_DATA = {};
2018-02-15 18:16:14 +00:00
/**
* When the display mode is 'Specific products' search for and add products to the block.
*
* @todo Add the functionality and everything.
*/
export class ProductsSpecificSelect extends React.Component {
/**
* Constructor.
*/
constructor( props ) {
super( props );
this.state = {
2018-02-16 19:40:19 +00:00
selectedProducts: props.selected_display_setting || [],
2018-02-15 18:16:14 +00:00
}
}
2018-02-22 18:48:34 +00:00
/**
* Add a product to the list of selected products.
*
* @param id int Product ID.
*/
2018-02-16 19:40:19 +00:00
addProduct( id ) {
2018-02-16 19:40:19 +00:00
let selectedProducts = this.state.selectedProducts;
selectedProducts.push( id );
2018-02-15 18:16:14 +00:00
2018-02-16 19:40:19 +00:00
this.setState( {
selectedProducts: selectedProducts
} );
/**
* We need to copy the existing data into a new array.
2018-02-26 22:52:12 +00:00
* We can't just push the new product onto the end of the existing array because Gutenberg seems
* to do some sort of check by reference to determine whether to *actually* update the attribute
* and will not update it if we just pass back the same array with an extra element on the end.
*/
this.props.update_display_setting_callback( selectedProducts.slice() );
2018-02-16 19:40:19 +00:00
}
2018-02-22 18:48:34 +00:00
/**
* Remove a product from the list of selected products.
*
* @param id int Product ID.
*/
2018-02-16 19:40:19 +00:00
removeProduct( id ) {
let oldProducts = this.state.selectedProducts;
let newProducts = [];
for ( let productId of oldProducts ) {
if ( productId !== id ) {
newProducts.push( productId );
}
}
2018-02-15 18:16:14 +00:00
this.setState( {
2018-02-16 19:40:19 +00:00
selectedProducts: newProducts
2018-02-15 18:16:14 +00:00
} );
2018-02-16 19:40:19 +00:00
this.props.update_display_setting_callback( newProducts );
2018-02-15 18:16:14 +00:00
}
2018-02-22 18:48:34 +00:00
/**
* Render the product specific select screen.
*/
2018-02-15 18:16:14 +00:00
render() {
return (
2018-02-26 22:52:12 +00:00
<div className="wc-products-list-card wc-products-list-card--specific">
<ProductsSpecificSearchField
addProductCallback={ this.addProduct.bind( this ) }
selectedProducts={ this.state.selectedProducts }
2018-02-22 18:48:34 +00:00
/>
2018-02-26 22:52:12 +00:00
<ProductSpecificSelectedProducts
2018-03-02 19:18:42 +00:00
productIds={ this.state.selectedProducts }
2018-02-26 22:52:12 +00:00
removeProductCallback={ this.removeProduct.bind( this ) }
2018-02-22 18:48:34 +00:00
/>
2018-02-16 19:40:19 +00:00
</div>
);
}
}
/**
* Product search area
*/
class ProductsSpecificSearchField extends React.Component {
2018-02-22 18:48:34 +00:00
/**
* Constructor.
*/
2018-02-16 19:40:19 +00:00
constructor( props ) {
super( props );
this.state = {
searchText: '',
}
this.updateSearchResults = this.updateSearchResults.bind( this );
this.setWrapperRef = this.setWrapperRef.bind( this );
this.handleClickOutside = this.handleClickOutside.bind( this );
}
/**
* Hook in the listener for closing menu when clicked outside.
*/
componentDidMount() {
document.addEventListener( 'mousedown', this.handleClickOutside );
}
/**
* Remove the listener for closing menu when clicked outside.
*/
componentWillUnmount() {
document.removeEventListener( 'mousedown', this.handleClickOutside );
}
/**
* Set the wrapper reference.
*
* @param node DOMNode
*/
setWrapperRef( node ) {
this.wrapperRef = node;
}
/**
* Close the menu when user clicks outside the search area.
*/
handleClickOutside( evt ) {
if ( this.wrapperRef && ! this.wrapperRef.contains( event.target ) ) {
this.setState( {
searchText: '',
} );
}
2018-02-16 19:40:19 +00:00
}
2018-02-22 18:48:34 +00:00
/**
* Event handler for updating results when text is typed into the input.
*
* @param evt Event object.
*/
2018-02-16 19:40:19 +00:00
updateSearchResults( evt ) {
this.setState( {
searchText: evt.target.value,
} );
}
2018-02-22 18:48:34 +00:00
/**
* Render the product search UI.
*/
2018-02-16 19:40:19 +00:00
render() {
return (
2018-02-26 22:52:12 +00:00
<div className="wc-products-list-card__search-wrapper" ref={ this.setWrapperRef }>
<input type="search"
className="wc-products-list-card__search"
2018-02-22 18:48:34 +00:00
value={ this.state.searchText }
placeholder={ __( 'Search for products to display' ) }
onChange={ this.updateSearchResults }
/>
<ProductSpecificSearchResults
searchString={ this.state.searchText }
addProductCallback={ this.props.addProductCallback }
selectedProducts={ this.props.selectedProducts }
/>
2018-02-15 18:16:14 +00:00
</div>
);
}
}
2018-02-16 19:40:19 +00:00
/**
2018-02-22 18:48:34 +00:00
* Render product search results based on the text entered into the textbox.
2018-02-16 19:40:19 +00:00
*/
const ProductSpecificSearchResults = withAPIData( ( props ) => {
if ( ! props.searchString.length ) {
return {
products: []
};
}
2018-02-15 18:16:14 +00:00
return {
2018-02-22 18:48:34 +00:00
products: '/wc/v2/products?per_page=10&search=' + props.searchString,
2018-02-15 18:16:14 +00:00
};
2018-02-22 18:48:34 +00:00
} )( ( { products, addProductCallback, selectedProducts } ) => {
2018-02-15 18:16:14 +00:00
if ( ! products.data ) {
2018-02-16 19:40:19 +00:00
return null;
2018-02-15 18:16:14 +00:00
}
if ( 0 === products.data.length ) {
return __( 'No products found' );
}
2018-03-02 19:18:42 +00:00
// Populate the cache.
for ( let product of products.data ) {
PRODUCT_DATA[ product.id ] = product;
}
2018-02-26 22:52:12 +00:00
return <ProductSpecificSearchResultsDropdown
2018-02-22 18:48:34 +00:00
products={ products.data }
addProductCallback={ addProductCallback }
selectedProducts={ selectedProducts }
/>
}
);
/**
* The dropdown of search results.
*/
class ProductSpecificSearchResultsDropdown extends React.Component {
/**
* Render dropdown.
*/
render() {
const { products, addProductCallback, selectedProducts } = this.props;
let productElements = [];
for ( let product of products ) {
if ( selectedProducts.includes( product.id ) ) {
continue;
}
productElements.push(
<CSSTransition
key={ product.slug }
classNames="wc-products-list-card__content--transition"
2018-02-22 18:48:34 +00:00
timeout={ { exit: 700 } }
>
2018-02-26 22:52:12 +00:00
<ProductSpecificSearchResultsDropdownElement
product={product}
addProductCallback={ addProductCallback }
2018-02-22 18:48:34 +00:00
/>
</CSSTransition>
);
}
2018-02-16 19:40:19 +00:00
return (
2018-02-26 22:52:12 +00:00
<div role="menu" className="wc-products-list-card__search-results" aria-orientation="vertical" aria-label="{ __( 'Products list' ) }">
2018-02-22 18:48:34 +00:00
<TransitionGroup>
{ productElements }
</TransitionGroup>
2018-02-26 22:52:12 +00:00
2018-02-16 19:40:19 +00:00
</div>
);
}
2018-02-22 18:48:34 +00:00
}
/**
* One search result.
*/
class ProductSpecificSearchResultsDropdownElement extends React.Component {
/**
* Constructor.
*/
constructor( props ) {
super( props );
this.state = {
clicked: false,
}
this.handleClick = this.handleClick.bind( this );
}
/**
* Add product to main list and change UI to show it was added.
*/
handleClick() {
this.setState( { clicked: true } );
this.props.addProductCallback( this.props.product.id );
}
/**
* Render one result in the search results.
*/
render() {
const product = this.props.product;
return (
<div className="wc-products-list-card__content" onClick={ this.handleClick }>
2018-02-26 22:52:12 +00:00
<img src={ product.images[0].src } />
<span className="wc-products-list-card__content-item-name">{ this.state.clicked ? __( 'Added' ) : product.name }</span>
<button type="button"
className="button-link"
id={ 'product-' + product.id } >
2018-02-26 22:52:12 +00:00
{ __( 'Add' ) }
2018-02-22 18:48:34 +00:00
</button>
2018-02-26 22:52:12 +00:00
</div>
2018-02-22 18:48:34 +00:00
);
}
}
2018-02-16 19:40:19 +00:00
/**
* List preview of selected products.
*/
const ProductSpecificSelectedProducts = withAPIData( ( props ) => {
2018-03-02 19:18:42 +00:00
if ( ! props.productIds.length ) {
2018-02-16 19:40:19 +00:00
return {
products: []
};
}
2018-03-02 19:18:42 +00:00
// Determine which products are not already in the cache and only fetch uncached products.
let uncachedProducts = [];
for( const productId of props.productIds ) {
if ( ! PRODUCT_DATA.hasOwnProperty( productId ) ) {
uncachedProducts.push( productId );
}
}
2018-02-16 19:40:19 +00:00
return {
2018-03-02 19:18:42 +00:00
products: uncachedProducts.length ? '/wc/v2/products?include=' + uncachedProducts.join( ',' ) : []
2018-02-15 18:16:14 +00:00
};
2018-03-02 19:18:42 +00:00
} )( ( { productIds, products, removeProductCallback } ) => {
// Add new products to cache.
if ( products.data ) {
for ( const product of products.data ) {
PRODUCT_DATA[ product.id ] = product;
}
2018-02-16 19:40:19 +00:00
}
2018-02-15 18:16:14 +00:00
2018-03-02 19:18:42 +00:00
if ( 0 === productIds.length ) {
2018-02-16 19:40:19 +00:00
return __( 'No products selected' );
}
2018-02-15 18:16:14 +00:00
2018-03-02 19:18:42 +00:00
const productElements = [];
for ( const productId of productIds ) {
// Skip products that aren't in the cache yet or failed to fetch.
if ( ! PRODUCT_DATA.hasOwnProperty( productId ) ) {
continue;
}
const productData = PRODUCT_DATA[ productId ];
productElements.push(
<li className="wc-products-list-card__item">
<div className="wc-products-list-card__content">
<img src={ productData.images[0].src } />
<span className="wc-products-list-card__content-item-name">{ productData.name }</span>
<button
type="button"
id={ 'product-' + productData.id }
onClick={ function() { removeProductCallback( productData.id ) } } >
<Dashicon icon={ 'no-alt' } />
</button>
</div>
</li>
);
}
2018-02-15 18:16:14 +00:00
return (
2018-02-27 11:56:18 +00:00
<div className="wc-products-list-card__results-wrapper">
<div role="menu" className="wc-products-list-card__results" aria-orientation="vertical" aria-label="{ __( 'Products list' ) }">
<ul>
2018-03-02 19:18:42 +00:00
{ productElements }
2018-02-27 11:56:18 +00:00
</ul>
</div>
2018-02-15 18:16:14 +00:00
</div>
);
}
);