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;