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 {Array}                      props.actionButtons               An array of button props for action buttons that display in each repeater row.
 * @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 {boolean}                    props.confirmDelete               If true, requires confirmation before deleting an item.
 * @param {Function}                   props.deleteConfirmationComponent Component to render for delete confirmation.
 * @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.inlineAdd                   If true, add buttons appear inline in each row instead of at bottom.
 * @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.onAdd                       The function to call when an item is added.
 * @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.showActions                 If the action button should be displayed.
 * @param {boolean}                    props.showActionsOnHover          If the actions should be displayed on hover.
 * @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.style                       The style of the repeater.
 * @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.
 */
const Repeater = ( {
	actionButtons = [],
	addButtonAttributes = {},
	addButtonClasses = [],
	blockHeaderAttributes = {},
	blockHeaderClasses = [],
	collapsible = false,
	collapsibleButtonAttributes = {},
	collapsibleButtonClasses = [],
	confirmDelete = false,
	deleteConfirmationComponent = null,
	customAttributes = {},
	customClasses = [],
	deleteButtonAttributes = {},
	deleteButtonClasses = [],
	downButtonAttributes = {},
	downButtonClasses = [],
	dragHandleAttributes = {},
	dragHandleClasses = [],
	fillContent = false,
	i18n = {
		addLabel: 'Add',
		beginDrag: 'Entering drag and drop for item %1$s.',
		copyLabel: 'Click to copy this item.',
		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.',
	},
	id = '',
	inlineAdd = false,
	isDraggable = false,
	isSortable = false,
	itemDraggable = false,
	itemAttributes = {},
	itemClasses = [],
	items = [],
	itemSpacing = '',
	maxItems = 0,
	minItems = 0,
	newItemState = {},
	onAdd = () => {},
	onChange = () => {},
	renderItem = () => {},
	screenReaderAttributes = {},
	screenReaderClasses = [],
	showActions = false,
	showActionsOnHover = false,
	showAdd = true,
	showArrows = false,
	showDelete = false,
	showDragHandle = false,
	spacing = '',
	style = 'regular',
	type = INLINE,
	upButtonAttributes = {},
	upButtonClasses = [],
} ) => {
	const [ screenReaderText, setScreenReaderText ] = useState( '' );

	const addItem = ( index = null ) => {
		const newItem = { repeater_item_id: `${ id }-${ Date.now() }`, ...newItemState };
		const updatedItems = Array.from( items );
		const insertIndex = index !== null ? index + 1 : updatedItems.length;
		updatedItems.splice( insertIndex, 0, newItem );
		onChange( updatedItems );
		onAdd( {
			id,
			index: insertIndex,
			itemId: newItem.repeater_item_id,
			state: updatedItems,
		} );

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

	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 = {
		customAttributes: {
			type: 'button',
		},
		label: i18n.addLabel,
		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,
	};

	const shouldShowAddButton = showAdd && (
		( ! inlineAdd && ( ! maxItems || items.length < maxItems ) ) ||
		( inlineAdd && items.length === 0 )
	);

	return (
		<div { ...componentProps }>
			<div { ...screenReaderProps } >
				{ screenReaderText }
			</div>
			{ items.map( ( item, index ) => (
				<RepeaterItem
					repeaterInstanceId={ id }
					actionButtons={ actionButtons }
					addButtonAttributes={ addButtonAttributes }
					addButtonClasses={ addButtonClasses }
					addItem={ inlineAdd ? () => addItem( index ) : undefined }
					blockContentTitle={ item?.repeater_item_block_content_title || '' }
					blockHeaderAttributes={ blockHeaderAttributes }
					blockHeaderClasses={ blockHeaderClasses }
					collapseItem={ collapseItem }
					collapsible={ collapsible }
					collapsibleButtonAttributes={ collapsibleButtonAttributes }
					collapsibleButtonClasses={ collapsibleButtonClasses }
					confirmDelete={ confirmDelete }
					deleteConfirmationComponent={ deleteConfirmationComponent }
					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 }
					inlineAdd={ inlineAdd }
					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 }
					maxItems={ maxItems }
					minItems={ minItems }
					moveItem={ moveItem }
					showActions={ showActions }
					showActionsOnHover={ showActionsOnHover }
					showAdd={ showAdd && inlineAdd }
					showArrows={ showArrows }
					showDelete={ showDelete }
					showDragHandle={ showDragHandle }
					speak={ setScreenReaderText }
					style={ style }
					type={ type }
					upButtonAttributes={ upButtonAttributes }
					upButtonClasses={ upButtonClasses }
				>
					{ renderItem( item, index ) }
				</RepeaterItem>
			) ) }
			{ shouldShowAddButton && <Button { ...addButtonProps } /> }
		</div>
	);
};

Repeater.propTypes = {
	actionButtons: PropTypes.array,
	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,
	] ),
	confirmDelete: PropTypes.bool,
	customAttributes: PropTypes.object,
	customClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	deleteButtonAttributes: PropTypes.object,
	deleteButtonClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	deleteConfirmationComponent: PropTypes.func,
	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,
	inlineAdd: PropTypes.bool,
	isDraggable: PropTypes.bool,
	isSortable: PropTypes.bool,
	itemAttributes: PropTypes.object,
	itemClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	itemDraggable: PropTypes.bool,
	items: PropTypes.array,
	itemSpacing: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.number,
		PropTypes.array,
		PropTypes.object,
	] ),
	maxItems: PropTypes.number,
	minItems: PropTypes.number,
	newItemState: PropTypes.object,
	onAdd: PropTypes.func,
	onChange: PropTypes.func,
	renderItem: PropTypes.elementType,
	screenReaderAttributes: PropTypes.object,
	screenReaderClasses: PropTypes.oneOfType( [
		PropTypes.element,
		PropTypes.func,
		PropTypes.array,
		PropTypes.string,
	] ),
	showActions: PropTypes.bool,
	showActionsOnHover: PropTypes.bool,
	showAdd: PropTypes.bool,
	showArrows: PropTypes.bool,
	showDelete: PropTypes.bool,
	showDragHandle: PropTypes.bool,
	spacing: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.number,
		PropTypes.array,
		PropTypes.object,
	] ),
	style: PropTypes.string,
	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;