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;