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

458 lines
10 KiB
React
Raw Normal View History

2018-02-15 18:16:14 +00:00
const { __ } = wp.i18n;
2018-09-05 18:30:46 +00:00
const { Toolbar, Dropdown, Dashicon } = wp.components;
const { apiFetch } = wp;
2018-02-15 18:16:14 +00:00
2018-04-06 20:12:26 +00:00
/**
* Get the identifier for an attribute. The identifier can be used to determine
* the slug or the ID of the attribute.
*
* @param string slug The attribute slug.
* @param int|numeric string id The attribute ID.
*/
export function getAttributeIdentifier( slug, id ) {
return slug + ',' + id;
}
/**
* Get the attribute slug from an identifier.
*
* @param string identifier The attribute identifier.
* @return string
*/
export function getAttributeSlug( identifier ) {
return identifier.split( ',' )[0];
}
/**
* Get the attribute ID from an identifier.
*
* @param string identifier The attribute identifier.
* @return numeric string
*/
export function getAttributeID( identifier ) {
return identifier.split( ',' )[1];
}
2018-02-15 18:16:14 +00:00
/**
* When the display mode is 'Attribute' search for and select product attributes to pull products from.
*/
export class ProductsAttributeSelect extends React.Component {
2018-02-20 19:47:50 +00:00
/**
* Constructor.
*/
constructor( props ) {
super( props );
/**
2018-04-06 20:12:26 +00:00
* The first item in props.selected_display_setting is the attribute slug and id separated by a comma.
* This is to work around limitations in the API which sometimes requires a slug and sometimes an id.
* The rest of the elements in selected_display_setting are the term ids for any selected terms.
*/
2018-02-20 19:47:50 +00:00
this.state = {
selectedAttribute: props.selected_display_setting.length ? props.selected_display_setting[0] : '',
selectedTerms: props.selected_display_setting.length > 1 ? props.selected_display_setting.slice( 1 ) : [],
2018-02-20 19:47:50 +00:00
filterQuery: '',
}
this.setSelectedAttribute = this.setSelectedAttribute.bind( this );
this.addTerm = this.addTerm.bind( this );
this.removeTerm = this.removeTerm.bind( this );
}
/**
* Set the selected attribute.
*
2018-04-06 20:12:26 +00:00
* @param identifier string Attribute slug and id separated by a comma.
2018-02-20 19:47:50 +00:00
*/
2018-04-06 20:12:26 +00:00
setSelectedAttribute( identifier ) {
2018-02-20 19:47:50 +00:00
this.setState( {
2018-04-06 20:12:26 +00:00
selectedAttribute: identifier,
2018-02-20 19:47:50 +00:00
selectedTerms: [],
} );
2018-04-06 20:12:26 +00:00
this.props.update_display_setting_callback( [ identifier ] );
2018-02-20 19:47:50 +00:00
}
/**
* Add a term to the selected attribute's terms.
*
2018-02-21 19:53:36 +00:00
* @param id int Term id.
2018-02-20 19:47:50 +00:00
*/
2018-02-21 19:53:36 +00:00
addTerm( id ) {
2018-02-20 19:47:50 +00:00
let terms = this.state.selectedTerms;
2018-02-21 19:53:36 +00:00
terms.push( id );
2018-02-20 19:47:50 +00:00
this.setState( {
selectedTerms: terms,
} );
let displaySetting = [ this.state.selectedAttribute ];
displaySetting = displaySetting.concat( terms );
this.props.update_display_setting_callback( displaySetting );
2018-02-20 19:47:50 +00:00
}
/**
* Remove a term from the selected attribute's terms.
*
2018-02-21 19:53:36 +00:00
* @param id int Term id.
2018-02-20 19:47:50 +00:00
*/
2018-02-21 19:53:36 +00:00
removeTerm( id ) {
2018-02-20 19:47:50 +00:00
let newTerms = [];
2018-02-21 19:53:36 +00:00
for ( let termId of this.state.selectedTerms ) {
if ( termId !== id ) {
newTerms.push( termId );
2018-02-20 19:47:50 +00:00
}
}
this.setState( {
selectedTerms: newTerms,
} );
let displaySetting = [ this.state.selectedAttribute ];
displaySetting = displaySetting.concat( newTerms );
this.props.update_display_setting_callback( displaySetting );
2018-02-20 19:47:50 +00:00
}
2018-02-21 19:53:36 +00:00
/**
* Update the search results when typing in the attributes box.
*
* @param evt Event object
*/
2018-02-21 19:30:47 +00:00
updateFilter( evt ) {
this.setState( {
filterQuery: evt.target.value,
} );
}
2018-02-20 19:47:50 +00:00
/**
* Render the whole section.
*/
2018-02-15 18:16:14 +00:00
render() {
return (
<div className="wc-products-list-card wc-products-list-card--taxonomy wc-products-list-card--taxonomy-atributes">
2018-02-21 19:30:47 +00:00
<ProductAttributeFilter updateFilter={ this.updateFilter.bind( this ) } />
2018-02-20 19:47:50 +00:00
<ProductAttributeList
selectedAttribute={ this.state.selectedAttribute }
selectedTerms={ this.state.selectedTerms }
2018-02-21 19:30:47 +00:00
filterQuery={ this.state.filterQuery }
2018-02-20 19:47:50 +00:00
setSelectedAttribute={ this.setSelectedAttribute.bind( this ) }
addTerm={ this.addTerm.bind( this ) }
removeTerm={ this.removeTerm.bind( this ) }
/>
2018-02-15 18:16:14 +00:00
</div>
);
}
}
2018-02-20 19:47:50 +00:00
/**
* Search area for filtering through the attributes list.
*/
2018-02-21 19:30:47 +00:00
const ProductAttributeFilter = ( props ) => {
2018-02-20 19:47:50 +00:00
return (
2018-04-06 22:57:20 +00:00
<div className="wc-products-list-card__input-wrapper">
<Dashicon icon="search" />
<input className="wc-products-list-card__search" type="search" placeholder={ __( 'Search for attributes' ) } onChange={ props.updateFilter } />
2018-02-20 19:47:50 +00:00
</div>
);
}
/**
* List of attributes.
*/
2018-09-06 15:40:50 +00:00
class ProductAttributeList extends React.Component {
/**
* Constructor
*/
constructor( props ) {
super( props );
this.state = {
attributes: [],
loaded: false,
query: '',
2018-02-20 19:47:50 +00:00
};
2018-09-06 15:40:50 +00:00
this.updatePreview = this.updatePreview.bind( this );
this.getQuery = this.getQuery.bind( this );
}
/**
* Get the preview when component is first loaded.
*/
componentDidMount() {
if ( this.getQuery() !== this.state.query ) {
this.updatePreview();
2018-02-20 19:47:50 +00:00
}
2018-09-06 15:40:50 +00:00
}
2018-02-20 19:47:50 +00:00
2018-09-06 15:40:50 +00:00
/**
* Update the preview when component is updated.
*/
componentDidUpdate() {
if ( this.getQuery() !== this.state.query && this.state.loaded ) {
this.updatePreview();
2018-02-20 19:47:50 +00:00
}
2018-09-06 15:40:50 +00:00
}
2018-02-20 19:47:50 +00:00
2018-09-06 15:40:50 +00:00
/**
* Get the endpoint for the current state of the component.
*
* @return string
*/
getQuery() {
const endpoint = '/wc/v2/products/attributes';
return endpoint;
}
/**
* Update the preview with the latest settings.
*/
updatePreview() {
const self = this;
const query = this.getQuery();
self.setState( {
loaded: false
} );
apiFetch( { path: query } ).then( attributes => {
self.setState( {
attributes: attributes,
loaded: true,
query: query
} );
} );
}
/**
* Render.
*/
render() {
const { selectedAttribute, filterQuery, selectedTerms, setSelectedAttribute, addTerm, removeTerm } = this.props;
if ( ! this.state.loaded ) {
return ( <ul><li>{ __( 'Loading' ) }</li></ul> );
}
if ( 0 === this.state.attributes.length ) {
return ( <ul><li>{ __( 'No attributes found' ) }</li></ul> );
}
2018-02-21 19:30:47 +00:00
const filter = filterQuery.toLowerCase();
2018-02-20 19:47:50 +00:00
let attributeElements = [];
2018-09-06 15:40:50 +00:00
for ( let attribute of this.state.attributes ) {
2018-02-21 19:30:47 +00:00
// Filter out attributes that don't match the search query.
if ( filter.length && -1 === attribute.name.toLowerCase().indexOf( filter ) ) {
continue;
}
2018-06-05 17:06:39 +00:00
attributeElements.push(
2018-09-06 15:40:50 +00:00
<ProductAttributeElement
attribute={ attribute }
selectedAttribute={ selectedAttribute }
selectedTerms={ selectedTerms }
2018-06-05 17:06:39 +00:00
setSelectedAttribute={ setSelectedAttribute}
addTerm={ addTerm }
2018-09-06 15:40:50 +00:00
removeTerm={ removeTerm }
2018-06-05 17:06:39 +00:00
/>
);
2018-02-20 19:47:50 +00:00
}
return (
<div className="wc-products-list-card__results">
2018-02-20 19:47:50 +00:00
{ attributeElements }
</div>
);
}
2018-09-06 15:40:50 +00:00
}
2018-02-20 19:47:50 +00:00
/**
2018-06-05 17:06:39 +00:00
* One product attribute.
2018-02-20 19:47:50 +00:00
*/
class ProductAttributeElement extends React.Component {
constructor( props ) {
super( props );
this.handleAttributeChange = this.handleAttributeChange.bind( this );
this.handleTermChange = this.handleTermChange.bind( this );
}
/**
* Propagate and reset values when the selected attribute is changed.
*
* @param evt Event object
*/
handleAttributeChange( evt ) {
if ( ! evt.target.checked ) {
2018-02-20 19:47:50 +00:00
return;
}
2018-04-06 20:12:26 +00:00
this.props.setSelectedAttribute( evt.target.value );
2018-02-20 19:47:50 +00:00
}
/**
* Add or remove selected terms.
*
* @param evt Event object
*/
handleTermChange( evt ) {
if ( evt.target.checked ) {
this.props.addTerm( evt.target.value );
} else {
this.props.removeTerm( evt.target.value );
}
}
render() {
2018-04-06 20:12:26 +00:00
const isSelected = this.props.selectedAttribute === getAttributeIdentifier( this.props.attribute.slug, this.props.attribute.id );
2018-02-20 19:47:50 +00:00
let attributeTerms = null;
if ( isSelected ) {
2018-06-05 17:06:39 +00:00
attributeTerms = <AttributeTerms
attribute={ this.props.attribute }
selectedTerms={ this.props.selectedTerms }
addTerm={ this.props.addTerm }
removeTerm={ this.props.removeTerm }
/>
2018-02-20 19:47:50 +00:00
}
let cssClasses = [ 'wc-products-list-card--taxonomy-atributes__atribute' ];
if ( isSelected ) {
cssClasses.push( 'wc-products-list-card__accordion-open' );
}
2018-02-20 19:47:50 +00:00
return (
<div className={ cssClasses.join( ' ' ) }>
<div>
<label className="wc-products-list-card__content">
<input type="radio"
2018-04-06 20:12:26 +00:00
value={ getAttributeIdentifier( this.props.attribute.slug, this.props.attribute.id ) }
onChange={ this.handleAttributeChange }
2018-02-20 19:47:50 +00:00
checked={ isSelected }
/>
{ this.props.attribute.name }
</label>
</div>
{ attributeTerms }
</div>
);
}
}
2018-06-05 17:06:39 +00:00
/**
* The list of terms in an attribute.
*/
2018-09-06 15:40:50 +00:00
class AttributeTerms extends React.Component {
/**
* Constructor
*/
constructor( props ) {
super( props );
this.state = {
terms: [],
loaded: false,
query: '',
2018-06-05 17:06:39 +00:00
};
2018-09-06 15:40:50 +00:00
this.updatePreview = this.updatePreview.bind( this );
this.getQuery = this.getQuery.bind( this );
}
/**
* Get the preview when component is first loaded.
*/
componentDidMount() {
if ( this.getQuery() !== this.state.query ) {
this.updatePreview();
}
}
/**
* Update the preview when component is updated.
*/
componentDidUpdate() {
if ( this.getQuery() !== this.state.query && this.state.loaded ) {
this.updatePreview();
}
}
/**
* Get the endpoint for the current state of the component.
*
* @return string
*/
getQuery() {
const endpoint = '/wc/v2/products/attributes/' + this.props.attribute.id + '/terms';
return endpoint;
}
/**
* Update the preview with the latest settings.
*/
updatePreview() {
const self = this;
const query = this.getQuery();
self.setState( {
loaded: false
} );
apiFetch( { path: query } ).then( terms => {
self.setState( {
terms: terms,
loaded: true,
query: query
} );
} );
}
/**
* Render.
*/
render() {
const { selectedTerms, attribute, addTerm, removeTerm } = this.props;
if ( ! this.state.loaded ) {
2018-06-05 17:06:39 +00:00
return ( <ul><li>{ __( 'Loading' ) }</li></ul> );
}
2018-09-06 15:40:50 +00:00
if ( 0 === this.state.terms.length ) {
2018-06-05 17:06:39 +00:00
return ( <ul><li>{ __( 'No terms found' ) }</li></ul> );
}
2018-09-06 15:40:50 +00:00
/**
* Add or remove selected terms.
*
* @param evt Event object
*/
2018-06-05 17:06:39 +00:00
function handleTermChange( evt ) {
if ( evt.target.checked ) {
addTerm( evt.target.value );
} else {
removeTerm( evt.target.value );
}
}
return (
<ul>
2018-09-06 15:40:50 +00:00
{ this.state.terms.map( ( term ) => (
2018-06-05 17:06:39 +00:00
<li className="wc-products-list-card__item">
<label className="wc-products-list-card__content">
<input type="checkbox"
value={ term.id }
onChange={ handleTermChange }
checked={ selectedTerms.includes( String( term.id ) ) }
/>
{ term.name }
<span className="wc-products-list-card__taxonomy-count">{ term.count }</span>
</label>
</li>
) ) }
</ul>
);
}
2018-09-06 15:40:50 +00:00
}