PATH:
home
/
letacommog
/
entrepro
/
wp-content
/
plugins
/
woo-gutenberg-products-block
/
assets
/
js
/
legacy
const { __ } = wp.i18n; const { Component, RawHTML } = wp.element; const { registerBlockType } = wp.blocks; const { InspectorControls, BlockControls } = wp.editor; const { Toolbar, Button, Dashicon, RangeControl, Tooltip, SelectControl } = wp.components; const { apiFetch } = wp; import './products-block.scss'; import { ProductsSpecificSelect } from './views/specific-select.jsx'; import { ProductsCategorySelect } from './views/category-select.jsx'; import { ProductsAttributeSelect, getAttributeSlug, getAttributeID } from './views/attribute-select.jsx'; /** * 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. * no_orderby - (optional) If set the setting does not allow orderby settings. */ 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', }, on_sale: { title: __( 'On sale' ), description: '', value: 'on_sale', }, best_selling: { title: __( 'Best sellers' ), description: '', value: 'best_selling', no_orderby: true, }, top_rated: { title: __( 'Top rated' ), description: '', value: 'top_rated', no_orderby: true, }, attribute: { title: __( 'Attribute' ), description: '', value: 'attribute', }, all: { title: __( 'All products' ), description: __( 'Display all products ordered chronologically, alphabetically, by price, by rating or by sales' ), value: 'all', }, }; /** * Returns whether or not a display scope supports orderby options. * * @param string display The display scope slug. * @return bool */ function supportsOrderby( display ) { return ! ( PRODUCTS_BLOCK_DISPLAY_SETTINGS.hasOwnProperty( display ) && PRODUCTS_BLOCK_DISPLAY_SETTINGS[ display ].hasOwnProperty( 'no_orderby' ) && PRODUCTS_BLOCK_DISPLAY_SETTINGS[ display ].no_orderby ); } /** * One option from the list of all available ways to display products. */ class ProductsBlockSettingsEditorDisplayOption extends Component { render() { let icon = 'arrow-right-alt2'; if ( 'filter' === this.props.value && this.props.extended ) { icon = 'arrow-down-alt2'; } let classes = 'wc-products-display-options__option wc-products-display-options__option--' + this.props.value; if ( this.props.current === this.props.value ) { icon = 'yes'; classes += ' wc-products-display-options__option--current'; } /* eslint-disable jsx-a11y/click-events-have-key-events */ /* eslint-disable jsx-a11y/no-static-element-interactions */ return ( <div className={ classes } onClick={ () => { if ( this.props.current !== this.props.value ) { this.props.update_display_callback( this.props.value ); } } } > <div className="wc-products-display-options__option-content"> <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> </div> ); /* eslint-enable */ } } /** * A list of all available ways to display products. */ class ProductsBlockSettingsEditorDisplayOptions extends Component { /** * 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( event ) { if ( this.wrapperRef && ! this.wrapperRef.contains( event.target ) && 'wc-products-settings-heading__change-button button-link' !== event.target.getAttribute( 'class' ) ) { this.props.closeMenu(); } } /** * Render the list of options. */ render() { let classes = 'wc-products-display-options'; if ( this.props.extended ) { classes += ' wc-products-display-options--extended'; } if ( this.props.existing ) { classes += ' wc-products-display-options--popover'; } const display_settings = []; for ( const setting_key in PRODUCTS_BLOCK_DISPLAY_SETTINGS ) { display_settings.push( <ProductsBlockSettingsEditorDisplayOption { ...PRODUCTS_BLOCK_DISPLAY_SETTINGS[ setting_key ] } update_display_callback={ this.props.update_display_callback } extended={ this.props.extended } current={ this.props.current } key={ setting_key } /> ); } const arrow = <span className="wc-products-display-options--popover__arrow"></span>; const description = <p className="wc-products-block-description">{ __( 'Choose which products you\'d like to display:' ) }</p>; return ( <div className={ classes } ref={ this.setWrapperRef }> { this.props.existing && arrow } { ! this.props.existing && description } { display_settings } </div> ); } } /** * The products block when in Edit mode. */ class ProductsBlockSettingsEditor extends Component { /** * Constructor. */ constructor( props ) { super( props ); this.state = { display: props.selected_display, menu_visible: props.selected_display ? false : true, expanded_group: '', }; this.updateDisplay = this.updateDisplay.bind( this ); this.closeMenu = this.closeMenu.bind( this ); } /** * Update the display settings for the block. * * @param value String */ updateDisplay( value ) { // If not a group update display. let new_state = { display: value, menu_visible: false, expanded_group: '', }; 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. new_state = { menu_visible: true, expanded_group: value, }; // 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 = ''; } } this.setState( new_state ); // Only update the display setting if a non-group setting was selected. if ( ! is_group ) { this.props.update_display_callback( value ); } } closeMenu() { this.setState( { menu_visible: false, } ); } /** * Render the display settings dropdown and any extra contextual settings. */ render() { let extra_settings = null; if ( 'specific' === this.state.display ) { extra_settings = <ProductsSpecificSelect { ...this.props } />; } else if ( 'category' === this.state.display ) { extra_settings = <ProductsCategorySelect { ...this.props } />; } else if ( 'attribute' === this.state.display ) { extra_settings = <ProductsAttributeSelect { ...this.props } />; } const menu = this.state.menu_visible ? <ProductsBlockSettingsEditorDisplayOptions extended={ this.state.expanded_group ? true : false } existing={ this.state.display ? true : false } current={ this.state.display } closeMenu={ this.closeMenu } update_display_callback={ this.updateDisplay } /> : null; let heading = null; if ( this.state.display ) { const group_options = [ 'featured', 'on_sale', 'attribute', 'best_selling', 'top_rated' ]; const should_group_expand = group_options.includes( this.state.display ) ? this.state.display : ''; const menu_link = ( <button type="button" className="wc-products-settings-heading__change-button button-link" onClick={ () => { this.setState( { menu_visible: ! this.state.menu_visible, expanded_group: should_group_expand, } ); } } > { __( 'Display different products' ) } </button> ); heading = ( <div className="wc-products-settings-heading"> <div className="wc-products-settings-heading__current"> { __( 'Displaying ' ) } <strong>{ __( PRODUCTS_BLOCK_DISPLAY_SETTINGS[ this.state.display ].title ) }</strong> </div> <div className="wc-products-settings-heading__change"> { menu_link } </div> </div> ); } let done_button = <button type="button" className="button wc-products-settings__footer-button" onClick={ this.props.done_callback }>{ __( 'Done' ) }</button>; if ( [ '', 'specific', 'category', 'attribute' ].includes( this.state.display ) && ! this.props.selected_display_setting.length ) { const done_tooltips = { '': __( 'Please select which products you\'d like to display' ), specific: __( 'Please search for and select products to display' ), category: __( 'Please select at least one category to display' ), attribute: __( 'Please select an attribute' ), }; done_button = ( <Tooltip text={ done_tooltips[ this.state.display ] } > <button type="button" className="button wc-products-settings__footer-button disabled">{ __( 'Done' ) }</button> </Tooltip> ); } return ( <div className={ 'wc-products-settings ' + ( this.state.expanded_group ? 'expanded-group-' + this.state.expanded_group : '' ) }> <h4 className="wc-products-settings__title"><Dashicon icon={ 'screenoptions' } /> { __( 'Products' ) }</h4> { heading } { menu } { extra_settings } <div className="wc-products-settings__footer"> { done_button } </div> </div> ); } } /** * One product in the product block preview. */ export class ProductPreview extends Component { render() { const { product } = this.props; let image = null; if ( product.images.length ) { image = <img src={ product.images[ 0 ].src } alt="" />; } return ( <div className="product-preview" key={ product.id + '-preview' } > { image } <div className="product-title">{ product.name }</div> <div className="product-price" dangerouslySetInnerHTML={ { __html: product.price_html } } /> <span className="product-add-to-cart">{ __( 'Add to cart' ) }</span> </div> ); } } /** * Renders a preview of what the block will look like with current settings. */ class ProductsBlockPreview extends Component { /** * Constructor */ constructor( props ) { super( props ); this.state = { products: [], loaded: false, query: '', }; this.updatePreview = this.updatePreview.bind( this ); this.getQuery = this.getQuery.bind( this ); } /** * Get the preview when component is first loaded. */ componentDidMount() { 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 { columns, rows, display, display_setting, orderby } = this.props.attributes; const query = { status: 'publish', per_page: rows * columns, }; if ( 'specific' === display ) { query.include = display_setting.join( ',' ); query.per_page = display_setting.length; } else if ( 'category' === display ) { query.category = display_setting.join( ',' ); } else if ( 'attribute' === display && display_setting.length ) { query.attribute = getAttributeSlug( display_setting[ 0 ] ); if ( display_setting.length > 1 ) { query.attribute_term = display_setting.slice( 1 ).join( ',' ); } } else if ( 'featured' === display ) { query.featured = 1; } else if ( 'on_sale' === display ) { query.on_sale = 1; } if ( supportsOrderby( display ) ) { if ( 'price_desc' === orderby ) { query.orderby = 'price'; query.order = 'desc'; } else if ( 'price_asc' === orderby ) { query.orderby = 'price'; query.order = 'asc'; } else if ( 'title' === orderby ) { query.orderby = 'title'; query.order = 'asc'; } else { query.orderby = orderby; } } let query_string = '?'; for ( const key of Object.keys( query ) ) { query_string += key + '=' + query[ key ] + '&'; } const endpoint = '/wc-pb/v3/products' + query_string; return endpoint; } /** * Update the preview with the latest settings. */ updatePreview() { const self = this; const query = this.getQuery(); self.setState( { loaded: false, query: query, } ); apiFetch( { path: query } ).then( ( products ) => { self.setState( { products: products, loaded: true, } ); } ); } /** * Render. */ render() { if ( ! this.state.loaded ) { return __( 'Loading' ); } if ( 0 === this.state.products.length ) { return __( 'No products found' ); } const classes = 'wc-products-block-preview cols-' + this.props.attributes.columns; const self = this; return ( <div className={ classes }> { this.state.products.map( ( product ) => ( <ProductPreview key={ product.id } product={ product } attributes={ self.props.attributes } /> ) ) } </div> ); } } /** * Information about current block settings rendered in the sidebar. */ class ProductsBlockSidebarInfo extends Component { /** * Constructor */ constructor( props ) { super( props ); this.state = { categoriesInfo: [], categoriesQuery: '', attributeInfo: false, attributeQuery: '', termsInfo: [], termsQuery: '', }; this.updateInfo = this.updateInfo.bind( this ); this.getQueries = this.getQueries.bind( this ); } /** * Populate info when component is first loaded. */ componentDidMount() { this.updateInfo(); } componentDidUpdate() { const queries = this.getQueries(); if ( this.state.categoriesQuery !== queries.categories || this.state.attributeQuery !== queries.attribute || this.state.termsQuery !== queries.terms ) { this.updateInfo(); } } /** * Get endpoints for the current state of the component. * * @return object */ getQueries() { const { display, display_setting } = this.props.attributes; const endpoints = { attribute: '', terms: '', categories: '', }; if ( 'attribute' === display && display_setting.length ) { const ID = getAttributeID( display_setting[ 0 ] ); const terms = display_setting.slice( 1 ).join( ', ' ); endpoints.attribute = '/wc-pb/v3/products/attributes/' + ID; if ( terms.length ) { endpoints.terms = '/wc-pb/v3/products/attributes/' + ID + '/terms?include=' + terms; } } else if ( 'category' === display && display_setting.length ) { endpoints.categories = '/wc-pb/v3/products/categories?include=' + display_setting.join( ',' ); } return endpoints; } /** * Get the latest info for the sidebar information area. */ updateInfo() { const self = this; const queries = this.getQueries(); this.setState( { categoriesQuery: queries.categories, attributeQuery: queries.attribute, termsQuery: queries.terms, } ); if ( queries.categories.length ) { apiFetch( { path: queries.categories } ).then( ( categories ) => { self.setState( { categoriesInfo: categories, } ); } ); } else { self.setState( { categoriesInfo: [], } ); } if ( queries.attribute.length ) { apiFetch( { path: queries.attribute } ).then( ( attribute ) => { self.setState( { attributeInfo: attribute, } ); } ); } else { self.setState( { attributeInfo: false, } ); } if ( queries.terms.length ) { apiFetch( { path: queries.terms } ).then( ( terms ) => { self.setState( { termsInfo: terms, } ); } ); } else { self.setState( { termsInfo: [], } ); } } /** * Render. */ render() { let descriptions = [ // Standard description of selected scope. PRODUCTS_BLOCK_DISPLAY_SETTINGS[ this.props.attributes.display ].title, ]; if ( this.state.categoriesInfo.length ) { let descriptionText = __( 'Product categories: ' ); const categories = []; for ( const category of this.state.categoriesInfo ) { categories.push( category.name ); } descriptionText += categories.join( ', ' ); descriptions = [ descriptionText, ]; // Description of attributes selected scope. } else if ( this.state.attributeInfo ) { descriptions = [ __( 'Attribute: ' ) + this.state.attributeInfo.name, ]; if ( this.state.termsInfo.length ) { let termDescriptionText = __( 'Terms: ' ); const terms = []; for ( const term of this.state.termsInfo ) { terms.push( term.name ); } termDescriptionText += terms.join( ', ' ); descriptions.push( termDescriptionText ); } } return ( <div> { descriptions.map( ( description, i ) => ( <div className="scope-description" key={ i }>{ description }</div> ) ) } </div> ); } } /** * The main products block UI. */ class ProductsBlock extends Component { /** * Constructor. */ constructor( props ) { super( props ); this.getInspectorControls = this.getInspectorControls.bind( this ); this.getToolbarControls = this.getToolbarControls.bind( this ); this.getBlockDescription = this.getBlockDescription.bind( this ); this.getPreview = this.getPreview.bind( this ); this.getSettingsEditor = this.getSettingsEditor.bind( this ); } /** * Get the components for the sidebar settings area that is rendered while focused on a Products block. * * @return Component */ getInspectorControls() { const { attributes, setAttributes } = this.props; const { rows, columns, display, orderby } = attributes; const columnControl = ( <RangeControl label={ __( 'Columns' ) } value={ columns } onChange={ ( value ) => setAttributes( { columns: value } ) } min={ wc_product_block_data.min_columns } max={ wc_product_block_data.max_columns } /> ); let orderControl = null; if ( supportsOrderby( display ) ) { orderControl = ( <SelectControl key="query-panel-select" label={ __( 'Order Products By' ) } value={ orderby } options={ [ { label: __( 'Newness - newest first' ), value: 'date', }, { label: __( 'Price - low to high' ), value: 'price_asc', }, { label: __( 'Price - high to low' ), value: 'price_desc', }, { label: __( 'Rating - highest first' ), value: 'rating', }, { label: __( 'Sales - most first' ), value: 'popularity', }, { label: __( 'Title - alphabetical' ), value: 'title', }, ] } onChange={ ( value ) => setAttributes( { orderby: value } ) } /> ); } // Row settings don't make sense for specific-selected products display. let rowControl = null; if ( 'specific' !== display ) { rowControl = ( <RangeControl label={ __( 'Rows' ) } value={ rows } onChange={ ( value ) => setAttributes( { rows: value } ) } min={ wc_product_block_data.min_rows } max={ wc_product_block_data.max_rows } /> ); } return ( <InspectorControls key="inspector"> { this.getBlockDescription() } <h3>{ __( 'Layout' ) }</h3> { columnControl } { rowControl } { orderControl } </InspectorControls> ); } /** * Get the components for the toolbar area that appears on top of the block when focused. * * @return Component */ getToolbarControls() { const props = this.props; const { attributes, setAttributes } = props; const { display, display_setting, edit_mode } = attributes; // Edit button should not do anything if valid product selection has not been made. const shouldDisableEditButton = [ '', 'specific', 'category', 'attribute' ].includes( display ) && ! display_setting.length; const editButton = [ { icon: 'edit', title: __( 'Edit' ), onClick: shouldDisableEditButton ? function() {} : () => setAttributes( { edit_mode: ! edit_mode } ), isActive: edit_mode, }, ]; return ( <BlockControls key="controls"> <Toolbar controls={ editButton } /> </BlockControls> ); } /** * Get a description of the current block settings. * * @return Component */ getBlockDescription() { const { attributes, setAttributes } = this.props; const { display } = attributes; if ( ! display.length ) { return null; } function editQuicklinkHandler() { setAttributes( { edit_mode: true, } ); // @todo center in view } let editQuickLink = null; if ( ! attributes.edit_mode ) { editQuickLink = ( <div className="wc-products-scope-description--edit-quicklink"> <Button isLink onClick={ editQuicklinkHandler }>{ __( 'Edit' ) }</Button> </div> ); } return ( <div className="wc-products-scope-descriptions"> <div className="wc-products-scope-details"> <h3>{ __( 'Current Source' ) }</h3> <ProductsBlockSidebarInfo attributes={ attributes } /> </div> { editQuickLink } </div> ); } /** * Get the block preview component for preview mode. * * @return Component */ getPreview() { return <ProductsBlockPreview key="preview" attributes={ this.props.attributes } />; } /** * Get the block edit component for edit mode. * * @return Component */ getSettingsEditor() { const { attributes, setAttributes } = this.props; const { display, display_setting } = attributes; const update_display_callback = ( value ) => { // These options have setting screens that need further input from the user, so keep edit mode open. const needsFurtherSettings = [ 'specific', 'attribute', 'category' ]; if ( display !== value ) { setAttributes( { display: value, display_setting: [], edit_mode: needsFurtherSettings.includes( value ), } ); } }; return ( <ProductsBlockSettingsEditor key="settings-editor" attributes={ attributes } selected_display={ display } selected_display_setting={ display_setting } update_display_callback={ update_display_callback } update_display_setting_callback={ ( value ) => setAttributes( { display_setting: value } ) } done_callback={ () => setAttributes( { edit_mode: false } ) } /> ); } render() { const { attributes } = this.props; const { edit_mode } = attributes; return [ this.getInspectorControls(), this.getToolbarControls(), edit_mode ? this.getSettingsEditor() : this.getPreview(), ]; } } /** * Register and run the products block. */ registerBlockType( 'woocommerce/products', { title: __( 'Products' ), icon: 'screenoptions', category: 'woocommerce', description: __( 'Display a grid of products from a variety of sources.' ), supports: { inserter: false, }, attributes: { /** * Number of columns. */ columns: { type: 'number', default: wc_product_block_data.default_columns, }, /** * Number of rows. */ rows: { type: 'number', default: wc_product_block_data.default_rows, }, /** * 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: [], }, /** * How to order the products: 'date', 'popularity', 'price_asc', 'price_desc' 'rating', 'title'. */ orderby: { type: 'string', default: 'date', }, /** * Whether the block is in edit or preview mode. */ edit_mode: { type: 'boolean', default: true, }, }, /** * Renders and manages the block. */ edit( props ) { return <ProductsBlock { ...props } />; }, /** * Save the block content in the post content. Block content is saved as a products shortcode. * * @return string */ save( props ) { const { rows, columns, display, display_setting, orderby } = props.attributes; const shortcode_atts = new Map(); if ( 'specific' !== display ) { shortcode_atts.set( 'limit', rows * columns ); } shortcode_atts.set( 'columns', columns ); if ( 'specific' === display ) { shortcode_atts.set( 'ids', display_setting.join( ',' ) ); } else if ( 'category' === display ) { shortcode_atts.set( 'category', display_setting.join( ',' ) ); } else if ( 'featured' === display ) { shortcode_atts.set( 'visibility', 'featured' ); } else if ( 'on_sale' === display ) { shortcode_atts.set( 'on_sale', '1' ); } else if ( 'best_selling' === display ) { shortcode_atts.set( 'best_selling', '1' ); } else if ( 'top_rated' === display ) { shortcode_atts.set( 'top_rated', '1' ); } else if ( 'attribute' === display ) { const attribute = display_setting.length ? getAttributeSlug( display_setting[ 0 ] ) : ''; const terms = display_setting.length > 1 ? display_setting.slice( 1 ).join( ',' ) : ''; shortcode_atts.set( 'attribute', attribute ); if ( terms.length ) { shortcode_atts.set( 'terms', terms ); } } if ( supportsOrderby( display ) ) { if ( 'price_desc' === orderby ) { shortcode_atts.set( 'orderby', 'price' ); shortcode_atts.set( 'order', 'DESC' ); } else if ( 'price_asc' === orderby ) { shortcode_atts.set( 'orderby', 'price' ); shortcode_atts.set( 'order', 'ASC' ); } else if ( 'date' === orderby ) { shortcode_atts.set( 'orderby', 'date' ); shortcode_atts.set( 'order', 'DESC' ); } else { shortcode_atts.set( 'orderby', orderby ); } } // Build the shortcode string out of the set shortcode attributes. let shortcode = '[products'; for ( const [ key, value ] of shortcode_atts ) { shortcode += ' ' + key + '="' + value + '"'; } shortcode += ']'; return <RawHTML>{ shortcode }</RawHTML>; }, } );
[+]
..
[-] products-block.jsx
[edit]
[-] products-block.scss
[edit]
[+]
views