2018-02-13 19:03:53 +00:00
const { _ _ } = wp . i18n ;
const { registerBlockType , InspectorControls , BlockControls } = wp . blocks ;
2018-02-23 14:57:27 +00:00
const { Toolbar , withAPIData , Dropdown , Dashicon } = wp . components ;
2018-02-13 19:03:53 +00:00
const { RangeControl , ToggleControl , SelectControl } = InspectorControls ;
2018-02-15 18:16:14 +00:00
import { ProductsSpecificSelect } from './views/specific-select.jsx' ;
import { ProductsCategorySelect } from './views/category-select.jsx' ;
import { ProductsAttributeSelect } from './views/attribute-select.jsx' ;
2018-02-15 17:42:24 +00:00
/ * *
* A setting has the following properties :
* title - Display title of the setting .
* description - Display description of the setting .
* value - Display setting slug to set when selected .
* group _container - ( optional ) If set the setting is a parent container .
* /
const PRODUCTS _BLOCK _DISPLAY _SETTINGS = {
'specific' : {
title : _ _ ( 'Individual products' ) ,
description : _ _ ( 'Hand-pick which products to display' ) ,
value : 'specific' ,
} ,
'category' : {
title : _ _ ( 'Product category' ) ,
description : _ _ ( 'Display products from a specific category or multiple categories' ) ,
value : 'category' ,
} ,
'filter' : {
title : _ _ ( 'Filter products' ) ,
description : _ _ ( 'E.g. featured products, or products with a specific attribute like size or color' ) ,
value : 'filter' ,
group _container : 'filter'
} ,
'featured' : {
title : _ _ ( 'Featured products' ) ,
description : '' ,
value : 'featured' ,
} ,
'best_sellers' : {
title : _ _ ( 'Best sellers' ) ,
description : '' ,
value : 'best_sellers' ,
} ,
'best_rated' : {
title : _ _ ( 'Best rated' ) ,
description : '' ,
value : 'best_rated' ,
} ,
'on_sale' : {
title : _ _ ( 'On sale' ) ,
description : '' ,
value : 'on_sale' ,
} ,
'attribute' : {
title : _ _ ( 'Attribute' ) ,
description : '' ,
value : 'attribute' ,
} ,
'all' : {
title : _ _ ( 'All products' ) ,
description : _ _ ( 'Display all products ordered chronologically' ) ,
value : 'all' ,
}
} ;
2018-02-13 19:03:53 +00:00
/ * *
* One option from the list of all available ways to display products .
* /
class ProductsBlockSettingsEditorDisplayOption extends React . Component {
render ( ) {
2018-02-23 14:57:27 +00:00
let icon = 'arrow-right-alt2' ;
if ( 'filter' === this . props . value && this . props . extended ) {
icon = 'arrow-down-alt2' ;
}
2018-02-13 19:03:53 +00:00
return (
2018-02-23 14:57:27 +00:00
< div className = { 'wc-products-display-options__option wc-products-display-options__option--' + this . props . value } onClick = { ( ) => { this . props . update _display _callback ( this . props . value ) } } >
2018-02-26 18:26:08 +00:00
< div className = "wc-products-display-options__option-content" >
2018-02-23 14:57:27 +00:00
< span className = "wc-products-display-options__option-title" > { this . props . title } < / span >
< p className = "wc-products-display-options__option-description" > { this . props . description } < / p >
< / div >
< div className = "wc-products-display-options__icon" >
< Dashicon icon = { icon } / >
< / div >
2018-02-13 19:03:53 +00:00
< / div >
) ;
}
}
/ * *
* A list of all available ways to display products .
* /
class ProductsBlockSettingsEditorDisplayOptions extends React . Component {
2018-02-23 18:22:05 +00:00
/ * *
* Constructor .
* /
constructor ( props ) {
super ( props ) ;
this . setWrapperRef = this . setWrapperRef . bind ( this ) ;
this . handleClickOutside = this . handleClickOutside . bind ( this ) ;
}
/ * *
* Hook in the listener for closing menu when clicked outside .
* /
componentDidMount ( ) {
if ( this . props . existing ) {
document . addEventListener ( 'mousedown' , this . handleClickOutside ) ;
}
}
/ * *
* Remove the listener for closing menu when clicked outside .
* /
componentWillUnmount ( ) {
if ( this . props . existing ) {
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 ) {
2018-02-26 18:26:08 +00:00
if ( this . wrapperRef && ! this . wrapperRef . contains ( event . target ) && 'wc-products-settings-heading__change-button button-link' !== event . target . getAttribute ( 'class' ) ) {
2018-02-23 18:22:05 +00:00
this . props . closeMenu ( ) ;
}
}
/ * *
* Render the list of options .
* /
2018-02-15 17:42:24 +00:00
render ( ) {
2018-02-23 14:57:27 +00:00
let classes = 'wc-products-display-options' ;
if ( this . props . extended ) {
classes += ' wc-products-display-options--extended' ;
}
2018-02-13 19:03:53 +00:00
if ( this . props . existing ) {
2018-02-23 14:57:27 +00:00
classes += ' wc-products-display-options--popover' ;
2018-02-13 19:03:53 +00:00
}
2018-02-15 17:42:24 +00:00
let display _settings = [ ] ;
for ( var setting _key in PRODUCTS _BLOCK _DISPLAY _SETTINGS ) {
2018-02-23 14:57:27 +00:00
display _settings . push ( < ProductsBlockSettingsEditorDisplayOption { ...PRODUCTS_BLOCK_DISPLAY_SETTINGS [ setting_key ] } update_display_callback = { this . props . update _display _callback } extended = { this . props . extended } / > ) ;
2018-02-15 17:42:24 +00:00
}
2018-02-23 14:57:27 +00:00
let arrow = < span className = "wc-products-display-options--popover__arrow" > < / span > ;
let description = < p className = "wc-products-block-description" > { _ _ ( 'Choose which products you\'d like to display:' ) } < / p > ;
2018-02-15 17:42:24 +00:00
2018-02-13 19:03:53 +00:00
return (
2018-02-23 18:22:05 +00:00
< div className = { classes } ref = { this . setWrapperRef } >
2018-02-23 14:57:27 +00:00
{ this . props . existing && arrow }
{ ! this . props . existing && description }
2018-02-15 17:42:24 +00:00
{ display _settings }
2018-02-13 19:03:53 +00:00
< / div >
) ;
}
}
/ * *
* The products block when in Edit mode .
* /
class ProductsBlockSettingsEditor extends React . Component {
/ * *
* Constructor .
* /
constructor ( props ) {
super ( props ) ;
this . state = {
display : props . selected _display ,
menu _visible : props . selected _display ? false : true ,
2018-02-15 17:42:24 +00:00
expanded _group : '' ,
2018-02-13 19:03:53 +00:00
}
this . updateDisplay = this . updateDisplay . bind ( this ) ;
2018-02-23 18:22:05 +00:00
this . closeMenu = this . closeMenu . bind ( this ) ;
2018-02-13 19:03:53 +00:00
}
/ * *
* Update the display settings for the block .
*
2018-02-15 17:42:24 +00:00
* @ param value String
2018-02-13 19:03:53 +00:00
* /
updateDisplay ( value ) {
2018-02-15 17:42:24 +00:00
// If not a group update display.
let new _state = {
2018-02-13 19:03:53 +00:00
display : value ,
menu _visible : false ,
2018-02-15 17:42:24 +00:00
expanded _group : '' ,
} ;
2018-02-15 19:54:03 +00:00
const is _group = 'undefined' !== PRODUCTS _BLOCK _DISPLAY _SETTINGS [ value ] . group _container && PRODUCTS _BLOCK _DISPLAY _SETTINGS [ value ] . group _container ;
if ( is _group ) {
// If the group has not been expanded, expand it.
2018-02-15 17:42:24 +00:00
new _state = {
menu _visible : true ,
expanded _group : value ,
}
2018-02-15 19:54:03 +00:00
// If the group has already been expanded, collapse it.
if ( this . state . expanded _group === PRODUCTS _BLOCK _DISPLAY _SETTINGS [ value ] . group _container ) {
new _state . expanded _group = '' ;
}
}
2018-02-13 19:03:53 +00:00
2018-02-15 17:42:24 +00:00
this . setState ( new _state ) ;
// Only update the display setting if a non-group setting was selected.
2018-02-15 19:54:03 +00:00
if ( ! is _group ) {
2018-02-15 17:42:24 +00:00
this . props . update _display _callback ( value ) ;
}
2018-02-13 19:03:53 +00:00
}
2018-02-23 18:22:05 +00:00
closeMenu ( ) {
this . setState ( {
menu _visible : false
} ) ;
}
2018-02-13 19:03:53 +00:00
/ * *
* Render the display settings dropdown and any extra contextual settings .
* /
render ( ) {
let extra _settings = null ;
if ( 'specific' === this . state . display ) {
2018-02-16 19:40:19 +00:00
extra _settings = < ProductsSpecificSelect { ...this.props } / > ;
2018-02-13 19:03:53 +00:00
} else if ( 'category' === this . state . display ) {
extra _settings = < ProductsCategorySelect { ...this.props } / > ;
2018-02-15 18:16:14 +00:00
} else if ( 'attribute' === this . state . display ) {
2018-02-20 19:47:50 +00:00
extra _settings = < ProductsAttributeSelect { ...this.props } / >
2018-02-13 19:03:53 +00:00
}
2018-02-26 18:26:08 +00:00
const menu = this . state . menu _visible ? < ProductsBlockSettingsEditorDisplayOptions extended = { this . state . expanded _group ? true : false } existing = { this . state . display ? true : false } closeMenu = { this . closeMenu } update_display_callback = { this . updateDisplay } / > : null ;
2018-02-13 19:03:53 +00:00
2018-02-15 17:42:24 +00:00
let heading = null ;
if ( this . state . display ) {
2018-02-23 14:57:27 +00:00
let menu _link = < button type = "button" className = "wc-products-settings-heading__change-button button-link" onClick = { ( ) => { this . setState ( { menu _visible : ! this . state . menu _visible } ) } } > { _ _ ( 'Display different products' ) } < / button > ;
2018-02-15 17:42:24 +00:00
heading = (
2018-02-23 14:57:27 +00:00
< div className = "wc-products-settings-heading" >
< div className = "wc-products-settings-heading__current" >
2018-02-15 17:42:24 +00:00
{ _ _ ( 'Displaying ' ) }
< strong > { _ _ ( PRODUCTS _BLOCK _DISPLAY _SETTINGS [ this . state . display ] . title ) } < / strong >
< / div >
2018-02-23 14:57:27 +00:00
< div className = "wc-products-settings-heading__change" >
2018-02-15 17:42:24 +00:00
{ menu _link }
< / div >
< / div >
) ;
2018-02-13 19:03:53 +00:00
}
return (
2018-02-26 17:10:45 +00:00
< div className = { 'wc-products-settings ' + ( this . state . expanded _group ? 'expanded-group-' + this . state . expanded _group : '' ) } >
< h4 className = "wc-products-settings__title" > < Dashicon icon = { 'universal-access-alt' } / > { _ _ ( 'Products' ) } < / h4 >
2018-02-13 19:03:53 +00:00
{ heading }
{ menu }
{ extra _settings }
2018-02-26 17:10:45 +00:00
< div className = "wc-products-settings__footer" >
< button type = "button" className = "button wc-products-settings__footer-button" onClick = { this . props . done _callback } > { _ _ ( 'Done' ) } < / button >
2018-02-13 19:03:53 +00:00
< / div >
< / div >
) ;
}
}
/ * *
* One product in the product block preview .
* /
class ProductPreview extends React . Component {
render ( ) {
const { attributes , product } = this . props ;
let image = null ;
if ( product . images . length ) {
image = < img src = { product . images [ 0 ] . src } / >
}
return (
< div className = "product-preview" >
{ image }
2018-02-22 20:11:53 +00:00
< div className = "product-title" > { product . name } < / div >
< div className = "product-price" > { product . price } < / div >
< span className = "product-add-to-cart" > { _ _ ( 'Add to cart' ) } < / span >
2018-02-13 19:03:53 +00:00
< / div >
) ;
}
}
/ * *
* Renders a preview of what the block will look like with current settings .
* /
const ProductsBlockPreview = withAPIData ( ( { attributes } ) => {
2018-02-23 18:46:25 +00:00
const { columns , rows , display , display _setting , block _layout } = attributes ;
2018-02-13 19:03:53 +00:00
let query = {
2018-02-22 15:33:18 +00:00
per _page : ( 'list' === block _layout ) ? rows : rows * columns ,
2018-02-13 19:03:53 +00:00
} ;
if ( 'specific' === display ) {
2018-02-23 20:05:44 +00:00
query . include = display _setting . join ( ',' ) ;
2018-02-13 19:03:53 +00:00
query . orderby = 'include' ;
} else if ( 'category' === display ) {
query . category = display _setting . join ( ',' ) ;
2018-02-21 19:53:36 +00:00
} else if ( 'attribute' === display && display _setting . length ) {
query . attribute = display _setting [ 0 ] ;
if ( display _setting . length > 1 ) {
query . attribute _term = display _setting . slice ( 1 ) . join ( ',' ) ;
}
2018-02-13 19:03:53 +00:00
}
let query _string = '?' ;
for ( const key of Object . keys ( query ) ) {
query _string += key + '=' + query [ key ] + '&' ;
}
return {
products : '/wc/v2/products' + query _string
} ;
} ) ( ( { products , attributes } ) => {
if ( ! products . data ) {
return _ _ ( 'Loading' ) ;
}
if ( 0 === products . data . length ) {
return _ _ ( 'No products found' ) ;
}
2018-02-22 15:33:18 +00:00
const classes = "wc-products-block-preview " + attributes . block _layout + " cols-" + attributes . columns ;
2018-02-13 19:03:53 +00:00
return (
< div className = { classes } >
{ products . data . map ( ( product ) => (
< ProductPreview key = { product . id } product = { product } attributes = { attributes } / >
) ) }
< / div >
) ;
} ) ;
/ * *
* Register and run the products block .
* /
registerBlockType ( 'woocommerce/products' , {
title : _ _ ( 'Products' ) ,
icon : 'universal-access-alt' , // @todo Needs a good icon.
category : 'widgets' ,
attributes : {
/ * *
* Layout to use . 'grid' or 'list' .
* /
2018-02-22 15:33:18 +00:00
block _layout : {
2018-02-13 19:03:53 +00:00
type : 'string' ,
default : 'grid' ,
} ,
/ * *
* Number of columns .
* /
columns : {
type : 'number' ,
default : 3 ,
} ,
/ * *
* Number of rows .
* /
rows : {
type : 'number' ,
default : 1 ,
} ,
/ * *
* What types of products to display . 'all' , 'specific' , or 'category' .
* /
display : {
type : 'string' ,
default : '' ,
} ,
/ * *
* Which products to display if 'display' is 'specific' or 'category' . Array of product ids or category slugs depending on setting .
* /
display _setting : {
type : 'array' ,
default : [ ] ,
} ,
/ * *
* Whether the block is in edit or preview mode .
* /
edit _mode : {
type : 'boolean' ,
default : true ,
} ,
} ,
/ * *
* Renders and manages the block .
* /
edit ( props ) {
const { attributes , className , focus , setAttributes , setFocus } = props ;
2018-02-23 18:46:25 +00:00
const { block _layout , rows , columns , display , display _setting , edit _mode } = attributes ;
2018-02-13 19:03:53 +00:00
/ * *
* Get the components for the sidebar settings area that is rendered while focused on a Products block .
*
* @ return Component
* /
function getInspectorControls ( ) {
return (
< InspectorControls key = "inspector" >
< h3 > { _ _ ( 'Layout' ) } < / h3 >
< RangeControl
label = { _ _ ( 'Columns' ) }
value = { columns }
onChange = { ( value ) => setAttributes ( { columns : value } ) }
min = { 1 }
max = { 6 }
/ >
< RangeControl
label = { _ _ ( 'Rows' ) }
value = { rows }
onChange = { ( value ) => setAttributes ( { rows : value } ) }
min = { 1 }
max = { 6 }
/ >
< / InspectorControls >
) ;
} ;
/ * *
* Get the components for the toolbar area that appears on top of the block when focused .
*
* @ return Component
* /
function getToolbarControls ( ) {
const layoutControls = [
{
icon : 'list-view' ,
title : _ _ ( 'List View' ) ,
2018-02-22 15:33:18 +00:00
onClick : ( ) => setAttributes ( { block _layout : 'list' } ) ,
isActive : 'list' === block _layout ,
2018-02-13 19:03:53 +00:00
} ,
{
icon : 'grid-view' ,
title : _ _ ( 'Grid View' ) ,
2018-02-22 15:33:18 +00:00
onClick : ( ) => setAttributes ( { block _layout : 'grid' } ) ,
isActive : 'grid' === block _layout ,
2018-02-13 19:03:53 +00:00
} ,
] ;
const editButton = [
{
icon : 'edit' ,
title : _ _ ( 'Edit' ) ,
onClick : ( ) => setAttributes ( { edit _mode : ! edit _mode } ) ,
isActive : edit _mode ,
} ,
] ;
return (
< BlockControls key = "controls" >
< Toolbar controls = { layoutControls } / >
< Toolbar controls = { editButton } / >
< / BlockControls >
) ;
}
/ * *
* Get the block preview component for preview mode .
*
* @ return Component
* /
function getPreview ( ) {
return < ProductsBlockPreview attributes = { attributes } / > ;
}
/ * *
* Get the block edit component for edit mode .
*
* @ return Component
* /
function getSettingsEditor ( ) {
return (
< ProductsBlockSettingsEditor
selected _display = { display }
selected _display _setting = { display _setting }
update _display _callback = { ( value ) => setAttributes ( { display : value } ) }
update _display _setting _callback = { ( value ) => setAttributes ( { display _setting : value } ) }
done _callback = { ( ) => setAttributes ( { edit _mode : false } ) }
/ >
) ;
}
return [
( ! ! focus ) ? getInspectorControls ( ) : null ,
( ! ! focus ) ? getToolbarControls ( ) : null ,
edit _mode ? getSettingsEditor ( ) : getPreview ( ) ,
] ;
} ,
/ * *
* Save the block content in the post content . Block content is saved as a products shortcode .
*
* @ return string
* /
save ( props ) {
2018-02-23 18:46:25 +00:00
const { block _layout , rows , columns , display , display _setting , className } = props . attributes ;
2018-02-13 19:03:53 +00:00
let shortcode _atts = new Map ( ) ;
2018-02-22 15:33:18 +00:00
shortcode _atts . set ( 'limit' , 'grid' === block _layout ? rows * columns : rows ) ;
shortcode _atts . set ( 'class' , 'list' === block _layout ? className + ' list-layout' : className ) ;
2018-02-13 19:03:53 +00:00
2018-02-22 15:33:18 +00:00
if ( 'grid' === block _layout ) {
2018-02-13 19:03:53 +00:00
shortcode _atts . set ( 'columns' , columns ) ;
}
if ( 'specific' === display ) {
shortcode _atts . set ( 'include' , display _setting . join ( ',' ) ) ;
}
if ( 'category' === display ) {
shortcode _atts . set ( 'category' , display _setting . join ( ',' ) ) ;
}
// Build the shortcode string out of the set shortcode attributes.
let shortcode = '[products' ;
for ( let [ key , value ] of shortcode _atts ) {
shortcode += ' ' + key + '="' + value + '"' ;
}
shortcode += ']' ;
return shortcode ;
} ,
} ) ;