modules_Repeater_index.js

import { React, PropTypes, classnames } from '@gravityforms/libraries';
import { spacerClasses, sprintf, trigger } from '@gravityforms/utils';
import { BLOCK, INLINE } from './constants';
import RepeaterItem from './RepeaterItem';
import Button from '../../elements/Button';

const { useState } = React;

/**
 * @module Repeater
 * @description A Repeater component with id wrapper.
 *
 * @since 4.7.0
 *
 * @param {object}                     props                             Component props.
 * @param {object}                     props.addButtonAttributes         Custom attributes for the add button.
 * @param {string|Array|object}        props.addButtonClasses            Custom classes for the add button.
 * @param {object}                     props.blockHeaderAttributes       Custom attributes for the block header (only used in block mode).
 * @param {string|Array|object}        props.blockHeaderClasses          Custom classes for the block header (only used in block mode).
 * @param {boolean}                    props.collapsible                 If the repeater is collapsible.
 * @param {object}                     props.collapsibleButtonAttributes Custom attributes for the collapsible button.
 * @param {string|Array|object}        props.collapsibleButtonClasses    Custom classes for the collapsible button.
 * @param {object}                     props.customAttributes            Custom attributes for the component.
 * @param {string|Array|object}        props.customClasses               Custom classes for the component.
 * @param {object}                     props.deleteButtonAttributes      Custom attributes for the delete button.
 * @param {string|Array|object}        props.deleteButtonClasses         Custom classes for the delete button.
 * @param {object}                     props.downButtonAttributes        Custom attributes for the down button.
 * @param {string|Array|object}        props.downButtonClasses           Custom classes for the down button.
 * @param {object}                     props.dragHandleAttributes        Custom attributes for the drag handle.
 * @param {string|Array|object}        props.dragHandleClasses           Custom classes for the drag handle.
 * @param {boolean}                    props.fillContent                 If the content should fill the available space.
 * @param {object}                     props.i18n                        Internationalization strings.
 * @param {string}                     props.id                          The id for the repeater. Required.
 * @param {boolean}                    props.isDraggable                 If the items are draggable.
 * @param {boolean}                    props.isSortable                  If the items are sortable.
 * @param {object}                     props.itemAttributes              Custom attributes for the item.
 * @param {string|Array|object}        props.itemClasses                 Custom classes for the item.
 * @param {boolean}                    props.itemDraggable               If the items are draggable.
 * @param {Array}                      props.items                       The items to display, managed by external state.
 * @param {string|number|Array|object} props.itemSpacing                 The spacing for the item, as a string, number, array, or object.
 * @param {number}                     props.maxItems                    The maximum number of items allowed.
 * @param {number}                     props.minItems                    The minimum number of items allowed.
 * @param {object}                     props.newItemState                The state for a new item.
 * @param {Function}                   props.onChange                    The function to call when the items change.
 * @param {Function}                   props.renderItem                  The function to render the item.
 * @param {object}                     props.screenReaderAttributes      Custom attributes for the screen reader text.
 * @param {string|Array|object}        props.screenReaderClasses         Custom classes for the screen reader text.
 * @param {boolean}                    props.showAdd                     If the add button should be displayed.
 * @param {boolean}                    props.showArrows                  If the arrows should be displayed.
 * @param {boolean}                    props.showDelete                  If the delete button should be displayed.
 * @param {boolean}                    props.showDragHandle              If the drag handle should be displayed.
 * @param {string|number|Array|object} props.spacing                     The spacing for the component, as a string, number, array, or object.
 * @param {string}                     props.type                        The type of the repeater.
 * @param {object}                     props.upButtonAttributes          Custom attributes for the up button.
 * @param {string|Array|object}        props.upButtonClasses             Custom classes for the up button.
 *
 * @return {JSX.Element} The Repeater component.
 *
 * @example
 * import Repeater from '@gravityforms/components/react/admin/modules/Repeater';
 *
 * return (
 *      <Repeater
 *          id="repeater-test"
 *          items={ [
 *              {
 *              some_key: 1,
 *              name: 'Item 1',
 *              repeater_item_collapsed: true,
 *              repeater_item_block_content_title: 'Block Content Title',
 *              repeater_item_id: 'repeater-test-1'
 *              },
 *          ] }
 *          spacing={ { '': 6, md: 8 } }
 *      />
 * );
 *
 */
const Repeater = ( {
	addButtonAttributes = {},
	addButtonClasses = [],
	blockHeaderAttributes = {},
	blockHeaderClasses = [],
	collapsible = false,
	collapsibleButtonAttributes = {},
	collapsibleButtonClasses = [],
	customAttributes = {},
	customClasses = [],
	deleteButtonAttributes = {},
	deleteButtonClasses = [],
	downButtonAttributes = {},
	downButtonClasses = [],
	dragHandleAttributes = {},
	dragHandleClasses = [],
	fillContent = false,
	i18n = {
		beginDrag: 'Entering drag and drop for item %1$s.',
		deleteLabel: 'Click to delete this item.',
		downLabel: 'Move item %1$s down.',
		dragLabel: 'Click to toggle drag and drop.',
		endDrag: 'Exiting drag and drop for item %1$s.',
		endDrop: 'Item %1$s moved to position %2$s.',
		moveItem: 'Moving item %1$s to %2$s',
		upLabel: 'Move item %1$s up.',
	},
	isDraggable = false,
	isSortable = false,
	itemDraggable = false,
	id = '',
	itemAttributes = {},
	itemClasses = [],
	items = [],
	itemSpacing = '',
	maxItems = 0,
	minItems = 0,
	newItemState = {},
	onChange = () => {},
	renderItem = () => {},
	screenReaderAttributes = {},
	screenReaderClasses = [],
	showAdd = true,
	showArrows = false,
	showDelete = false,
	showDragHandle = false,
	spacing = '',
	type = INLINE,
	upButtonAttributes = {},
	upButtonClasses = [],
} ) => {
	const [ screenReaderText, setScreenReaderText ] = useState( '' );

	const addItem = () => {
		const newItem = { repeater_item_id: `${ id }-${ Date.now() }`, ...newItemState };
		onChange( [ ...items, newItem ] );

		trigger( { event: 'gform/repeater/item_added', native: false, data: {
			id,
			index: items.length,
			itemId: newItem.repeater_item_id,
			state: [ ...items, newItem ],
		} } );
	};

	const collapseItem = ( index, itemId ) => {
		const updatedItems = Array.from( items );
		updatedItems[ index ].repeater_item_collapsed = ! updatedItems[ index ].repeater_item_collapsed;
		onChange( updatedItems );

		trigger( { event: 'gform/repeater/item_collapsed', native: false, data: {
			id,
			index,
			itemId,
			state: updatedItems,
		} } );
	};

	const deleteItem = ( index, itemId ) => {
		const updatedItems = Array.from( items );
		updatedItems.splice( index, 1 );
		onChange( updatedItems );

		trigger( { event: 'gform/repeater/item_deleted', native: false, data: {
			id,
			index,
			itemId,
			state: updatedItems,
		} } );
	};

	const moveItem = ( fromIndex, toIndex, itemId ) => {
		const updatedItems = Array.from( items );
		const [ movedItem ] = updatedItems.splice( fromIndex, 1 );
		updatedItems.splice( toIndex, 0, movedItem );
		setScreenReaderText( sprintf( i18n.moveItem, itemId, toIndex ) );
		onChange( updatedItems );

		trigger( { event: 'gform/repeater/item_moved', native: false, data: {
			fromIndex,
			id,
			itemId,
			item: movedItem,
			state: updatedItems,
			toIndex,
		} } );
	};

	const addButtonProps = {
		label: 'Add',
		type: 'white',
		iconPosition: 'leading',
		onClick: addItem,
		iconPrefix: 'gravity-component-icon',
		icon: 'plus-regular',
		size: 'size-height-m',
		customClasses: classnames( [
			'gform-repeater__add-button',
		], addButtonClasses ),
		disabled: !! ( maxItems && items.length >= maxItems ),
		...addButtonAttributes,
	};

	const screenReaderProps = {
		id: `${ id }-repeater-screen-reader-text`,
		className: classnames( [
			'gform-repeater__screen-reader-text',
			'gform-visually-hidden',
		], screenReaderClasses ),
		'aria-live': 'polite',
		...screenReaderAttributes,
	};

	const componentProps = {
		className: classnames( {
			'gform-repeater': true,
			'gform-repeater--collapsible': collapsible,
			[ `gform-repeater--type-${ type }` ]: true,
			...spacerClasses( spacing ),
		}, customClasses ),
		id: `${ id }-list-wrapper`,
		...customAttributes,
	};

	return (
		<div { ...componentProps }>
			<div { ...screenReaderProps } >
				{ screenReaderText }
			</div>
			{ items.map( ( item, index ) => (
				<RepeaterItem
					blockContentTitle={ item?.repeater_item_block_content_title || '' }
					blockHeaderAttributes={ blockHeaderAttributes }
					blockHeaderClasses={ blockHeaderClasses }
					collapseItem={ collapseItem }
					collapsible={ collapsible }
					collapsibleButtonAttributes={ collapsibleButtonAttributes }
					collapsibleButtonClasses={ collapsibleButtonClasses }
					deleteButtonAttributes={ deleteButtonAttributes }
					deleteButtonClasses={ deleteButtonClasses }
					deleteItem={ deleteItem }
					downButtonAttributes={ downButtonAttributes }
					downButtonClasses={ downButtonClasses }
					dragHandleAttributes={ dragHandleAttributes }
					dragHandleClasses={ dragHandleClasses }
					fillContent={ fillContent }
					i18n={ i18n }
					id={ item.repeater_item_id }
					index={ index }
					isCollapsed={ item.repeater_item_collapsed }
					isDraggable={ isDraggable }
					isSortable={ isSortable }
					itemAttributes={ itemAttributes }
					itemClasses={ itemClasses }
					itemCount={ items.length }
					itemDraggable={ itemDraggable }
					itemSpacing={ itemSpacing }
					key={ item.repeater_item_id }
					minItems={ minItems }
					moveItem={ moveItem }
					showArrows={ showArrows }
					showDelete={ showDelete }
					showDragHandle={ showDragHandle }
					speak={ setScreenReaderText }
					type={ type }
					upButtonAttributes={ upButtonAttributes }
					upButtonClasses={ upButtonClasses }
				>
					{ renderItem( item, index ) }
				</RepeaterItem>
			) ) }
			{ showAdd && <Button { ...addButtonProps } /> }
		</div>
	);
};

Repeater.propTypes = {
	addButtonAttributes: PropTypes.object,
	addButtonClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	blockHeaderAttributes: PropTypes.object,
	blockHeaderClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	collapsible: PropTypes.bool,
	collapsibleButtonAttributes: PropTypes.object,
	collapsibleButtonClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	customAttributes: PropTypes.object,
	customClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	deleteButtonAttributes: PropTypes.object,
	deleteButtonClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	downButtonAttributes: PropTypes.object,
	downButtonClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	dragHandleAttributes: PropTypes.object,
	dragHandleClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	fillContent: PropTypes.bool,
	i18n: PropTypes.object,
	id: PropTypes.string.isRequired,
	items: PropTypes.array,
	isDraggable: PropTypes.bool,
	isSortable: PropTypes.bool,
	itemAttributes: PropTypes.object,
	itemClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	itemDraggable: PropTypes.bool,
	itemSpacing: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.number,
		PropTypes.array,
		PropTypes.object,
	] ),
	maxItems: PropTypes.number,
	minItems: PropTypes.number,
	newItemState: PropTypes.object,
	onChange: PropTypes.func,
	renderItem: PropTypes.elementType,
	screenReaderAttributes: PropTypes.object,
	screenReaderClasses: PropTypes.oneOfType( [
		PropTypes.element,
		PropTypes.func,
		PropTypes.array,
		PropTypes.string,
	] ),
	showAdd: PropTypes.bool,
	showArrows: PropTypes.bool,
	showDelete: PropTypes.bool,
	showDragHandle: PropTypes.bool,
	spacing: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.number,
		PropTypes.array,
		PropTypes.object,
	] ),
	type: PropTypes.oneOf( [ BLOCK, INLINE ] ),
	upButtonAttributes: PropTypes.object,
	upButtonClasses: PropTypes.oneOfType( [
		PropTypes.element,
		PropTypes.func,
		PropTypes.array,
		PropTypes.string,
	] ),
};

Repeater.displayName = 'Repeater';

export default Repeater;