const { __ } = wp.i18n; const { registerBlockType, InspectorControls, BlockControls } = wp.blocks; const { Toolbar, withAPIData, Dropdown } = wp.components; const { RangeControl, ToggleControl, SelectControl } = InspectorControls; /** * When the display mode is 'Specific products' search for and add products to the block. * * @todo Add the functionality and everything. */ class ProductsSpecificSelect extends React.Component { /** * Constructor. */ constructor( props ) { super( props ); this.state = { selectedProducts: props.selected_display_setting, } } selectProduct( evt ) { evt.preventDefault(); let selectProduct = this.state.selectProduct; this.setState( { selectProduct: selectProduct } ); } render() { return (
( ) } renderContent={ () => (
) } />
); } } const ProductSpecifcSearch = withAPIData( ( props ) => { return { products: '/wc/v2/products?per_page=10' }; } )( ( { products } ) => { if ( ! products.data ) { return __( 'Loading' ); } if ( 0 === products.data.length ) { return __( 'No products found' ); } const ProductsList = ( { products } ) => { return ( products.length > 0 ) && ( ); }; let productsData = products.data; return (
); } ); const ProductsSpecificList = ( { selectedProducts } ) => { if ( ! selectedProducts || 0 === selectedProducts.length ) { return __( 'No products selected found' ); } const classes = "wc-products-block-preview"; const attributes = {}; return (
{ selectedProducts.data.map( ( product ) => ( ) ) }
); } /** * When the display mode is 'Product category' search for and select product categories to pull products from. */ class ProductsCategorySelect extends React.Component { /** * Constructor. */ constructor( props ) { super( props ); this.state = { selectedCategories: props.selected_display_setting, filterQuery: '' } this.checkboxChange = this.checkboxChange.bind( this ); this.filterResults = this.filterResults.bind( this ); } /** * Handle checkbox toggle. * * @param Event object evt */ checkboxChange( evt ) { let selectedCategories = this.state.selectedCategories; if ( evt.target.checked && ! selectedCategories.includes( parseInt( evt.target.value, 10 ) ) ) { selectedCategories.push( parseInt( evt.target.value, 10 ) ); } else if ( ! evt.target.checked ) { selectedCategories = selectedCategories.filter( category => category !== parseInt( evt.target.value, 10 ) ); } this.setState( { selectedCategories: selectedCategories } ); this.props.update_display_setting_callback( selectedCategories ); } /** * Filter categories. * * @param Event object evt */ filterResults( evt ) { this.setState( { filterQuery: evt.target.value } ); } /** * Render the list of categories and the search input. */ render() { return (
); } } /** * The category search input. */ const ProductCategoryFilter = ( { filterResults } ) => { return (
); } /** * Fetch and build a tree of product categories. */ const ProductCategoryList = withAPIData( ( props ) => { return { categories: '/wc/v2/products/categories' }; } )( ( { categories, filterQuery, selectedCategories, checkboxChange } ) => { if ( ! categories.data ) { return __( 'Loading' ); } if ( 0 === categories.data.length ) { return __( 'No categories found' ); } const CategoryTree = ( { categories, parent } ) => { let filteredCategories = categories.filter( ( category ) => category.parent === parent ); return ( filteredCategories.length > 0 ) && ( ); }; let categoriesData = categories.data; if ( '' !== filterQuery ) { categoriesData = categoriesData.filter( category => category.slug.includes( filterQuery.toLowerCase() ) ); } return (
); } ); /** * One option from the list of all available ways to display products. */ class ProductsBlockSettingsEditorDisplayOption extends React.Component { render() { return (
{ this.props.update_display_callback( this.props.value ) } } >

{ this.props.title }

{ this.props.description }

); } } /** * A list of all available ways to display products. */ class ProductsBlockSettingsEditorDisplayOptions extends React.Component { render() { const products_block_display_settings = [ { title: __( 'All' ), description: __( 'All products' ), value: 'all', }, { title: __( 'Specific' ), description: __( 'Hand-picked products' ), value: 'specific', }, { title: __( 'Category' ), description: __( 'Products from a specific category' ), value: 'category', } ]; let classes = 'display-settings-container'; if ( this.props.existing ) { classes += ' existing'; } return (

{ __( 'Select the scope for products to display:' ) }

{ products_block_display_settings.map( ( setting ) => ) }
); } } /** * 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, } this.updateDisplay = this.updateDisplay.bind( this ); } /** * Update the display settings for the block. * * @param Event object evt */ updateDisplay( value ) { this.setState( { display: value, menu_visible: false, } ); this.props.update_display_callback( value ); } /** * Render the display settings dropdown and any extra contextual settings. */ render() { let extra_settings = null; if ( 'specific' === this.state.display ) { extra_settings = ; } else if ( 'category' === this.state.display ) { extra_settings = ; } const menu = this.state.menu_visible ? : null; let heading =

{ __( 'Products' ) }

; if ( this.state.display && ! this.state.menu_visible ) { heading =

{ __( 'Displaying ' + this.state.display ) } { this.setState( { menu_visible: true } ) } }>{ __( 'Change' ) }

; } else if ( this.state.display ) { heading =

{ __( 'Displaying ' + this.state.display ) } { this.setState( { menu_visible: false } ) } }>{ __( 'Cancel' ) }

; } return (
{ heading } { menu } { extra_settings }
); } } /** * 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 = } let title = null; if ( attributes.display_title ) { title =
{ product.name }
} let price = null; if ( attributes.display_price ) { price =
{ product.price }
} let add_to_cart = null; if ( attributes.display_add_to_cart ) { add_to_cart = { __( 'Add to cart' ) } } return (
{ image } { title } { price } { add_to_cart }
); } } /** * Renders a preview of what the block will look like with current settings. */ const ProductsBlockPreview = withAPIData( ( { attributes } ) => { const { columns, rows, order, display, display_setting, layout } = attributes; let query = { per_page: ( 'list' === layout ) ? rows : rows * columns, orderby: order }; // @todo These will likely need to be modified to work with the final version of the category/product picker attributes. if ( 'specific' === display ) { query.include = JSON.stringify( display_setting ); query.orderby = 'include'; } else if ( 'category' === display ) { query.category = display_setting.join( ',' ); } 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' ); } const classes = "wc-products-block-preview " + attributes.layout + " cols-" + attributes.columns; return (
{ products.data.map( ( product ) => ( ) ) }
); } ); /** * 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'. */ layout: { type: 'string', default: 'grid', }, /** * Number of columns. */ columns: { type: 'number', default: 3, }, /** * Number of rows. */ rows: { type: 'number', default: 1, }, /** * Whether to display product titles. */ display_title: { type: 'boolean', default: true, }, /** * Whether to display prices. */ display_price: { type: 'boolean', default: true, }, /** * Whether to display Add to Cart buttons. */ display_add_to_cart: { type: 'boolean', default: false, }, /** * Order to use for products. 'date', or 'title'. */ order: { type: 'string', default: 'date', }, /** * 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; const { layout, rows, columns, display_title, display_price, display_add_to_cart, order, display, display_setting, edit_mode } = attributes; /** * Get the components for the sidebar settings area that is rendered while focused on a Products block. * * @return Component */ function getInspectorControls() { return (

{ __( 'Layout' ) }

setAttributes( { columns: value } ) } min={ 1 } max={ 6 } /> setAttributes( { rows: value } ) } min={ 1 } max={ 6 } /> setAttributes( { display_title: ! display_title } ) } /> setAttributes( { display_price: ! display_price } ) } /> setAttributes( { display_add_to_cart: ! display_add_to_cart } ) } /> setAttributes( { order: value } ) } />
); }; /** * 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' ), onClick: () => setAttributes( { layout: 'list' } ), isActive: layout === 'list', }, { icon: 'grid-view', title: __( 'Grid View' ), onClick: () => setAttributes( { layout: 'grid' } ), isActive: layout === 'grid', }, ]; const editButton = [ { icon: 'edit', title: __( 'Edit' ), onClick: () => setAttributes( { edit_mode: ! edit_mode } ), isActive: edit_mode, }, ]; return ( ); } /** * Get the block preview component for preview mode. * * @return Component */ function getPreview() { return ; } /** * Get the block edit component for edit mode. * * @return Component */ function getSettingsEditor() { return ( 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 ) { const { layout, rows, columns, display_title, display_price, display_add_to_cart, order, display, display_setting, className } = props.attributes; let shortcode_atts = new Map(); shortcode_atts.set( 'orderby', order ); shortcode_atts.set( 'limit', 'grid' === layout ? rows * columns : rows ); shortcode_atts.set( 'class', 'list' === layout ? className + ' list-layout' : className ); if ( 'grid' === layout ) { shortcode_atts.set( 'columns', columns ); } if ( ! display_title ) { shortcode_atts.set( 'show_title', 0 ); } if ( ! display_price ) { shortcode_atts.set( 'show_price', 0 ); } if ( ! display_add_to_cart ) { shortcode_atts.set( 'show_add_to_cart', 0 ); } 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; }, } );