@gravityforms/components
Version:
UI components for use in Gravity Forms development. Both React and vanilla js flavors.
535 lines (489 loc) • 14.9 kB
JavaScript
import { React, ReactDND, PropTypes, classnames } from '@gravityforms/libraries';
import { ConditionalWrapper } from '@gravityforms/react-utils';
import { spacerClasses, sprintf } from '@gravityforms/utils';
import { UP, DOWN, BLOCK, INLINE } from './constants';
import { ARROW_UP, ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT, TAB } from '../../utils/keymap';
import itemTypes from './item-types';
import Button from '../../elements/Button';
import Text from '../../elements/Text';
const { useRef, useState } = React;
const { useDrag, useDrop } = ReactDND;
const RepeaterItem = ( {
addItem = undefined,
actionButtons = [],
addButtonAttributes = {},
addButtonClasses = [],
blockContentTitle = '',
blockHeaderAttributes = {},
blockHeaderClasses = [],
children = null,
collapseItem = () => {},
collapsible = false,
collapsibleButtonAttributes = {},
collapsibleButtonClasses = [],
confirmDelete = false,
deleteConfirmationComponent = null,
deleteButtonAttributes = {},
deleteButtonClasses = [],
deleteItem = () => {},
downButtonAttributes = {},
downButtonClasses = [],
dragHandleAttributes = {},
dragHandleClasses = [],
fillContent = false,
i18n = {},
id = '',
index = 0,
inlineAdd = false,
isCollapsed = false,
isDraggable = false,
isSortable = false,
itemAttributes = {},
itemClasses = [],
itemCount = 0,
itemDraggable = false,
itemSpacing = '',
maxItems = 0,
minItems = 0,
moveItem = () => {},
repeaterInstanceId = '',
showActions = false,
showActionsOnHover = false,
showAdd = false,
showArrows = false,
showDelete = false,
showDragHandle = false,
speak = () => {},
style = 'regular',
type = INLINE,
upButtonAttributes = {},
upButtonClasses = [],
} ) => {
const ref = useRef( null );
const dragHandleRef = useRef( null );
const startIndexRef = useRef( null );
const [ keyboardDragActive, setKeyboardDragActive ] = useState( false );
const inlineButtonSize = style === 'minimal' && type === INLINE ? 'size-height-s' : 'size-height-m';
const [ isConfirmingDelete, setIsConfirmingDelete ] = useState( false );
const dndItemType = `${ itemTypes.REPEATER_ITEM }_${ repeaterInstanceId }`;
const handleKeyboardNav = ( event ) => {
event.stopPropagation();
if ( event.key === TAB && keyboardDragActive ) {
event.preventDefault();
return;
}
if ( ! keyboardDragActive ) {
return;
}
if ( ( event.key === ARROW_UP || event.key === ARROW_LEFT ) ) {
moveItem( index, index - 1, id );
return;
}
if ( ( event.key === ARROW_DOWN || event.key === ARROW_RIGHT ) ) {
moveItem( index, index + 1, id );
}
};
const beginDragging = () => {
speak( sprintf( i18n.beginDrag, id ) );
setKeyboardDragActive( true );
};
const endDragging = () => {
speak( sprintf( i18n.endDrag, id ) );
setKeyboardDragActive( false );
};
const toggleDragging = () => {
if ( keyboardDragActive ) {
endDragging();
} else {
beginDragging();
}
};
const handleCollapsibleClick = () => {
collapseItem( index, id );
};
const handleArrowPress = ( event, direction ) => {
const toIndex = direction === UP ? index - 1 : index + 1;
moveItem( index, toIndex, id );
};
const handleDelete = () => {
if ( confirmDelete && deleteConfirmationComponent ) {
setIsConfirmingDelete( true ); // Trigger confirmation dialog
} else {
deleteItem( index, id ); // Proceed directly if no confirmation required
}
};
const handleConfirmDelete = ( confirmed ) => {
setIsConfirmingDelete( false ); // Reset confirmation state
if ( confirmed ) {
deleteItem( index, id ); // Proceed with deletion if confirmed
}
};
const [ { isDragging }, drag, preview ] = useDrag( {
type: dndItemType,
item: { id, index, repeaterInstanceId },
collect: ( monitor ) => ( {
isDragging: monitor.isDragging(),
} ),
} );
const [ { handlerId }, drop ] = useDrop( {
accept: dndItemType,
collect( monitor ) {
return {
handlerId: monitor.getHandlerId(),
isOver: monitor.isOver(),
};
},
hover( item, monitor ) {
if ( item.repeaterInstanceId !== repeaterInstanceId ) {
return;
}
if ( ! ref.current || item.index === index ) {
return;
}
const dragIndex = item.index;
const hoverIndex = index;
if ( startIndexRef.current === null ) {
startIndexRef.current = dragIndex;
}
if ( dragIndex === hoverIndex ) {
return;
}
const hoverBoundingRect = ref.current?.getBoundingClientRect();
const hoverMiddleY = ( hoverBoundingRect.bottom - hoverBoundingRect.top ) / 2;
const clientOffset = monitor.getClientOffset();
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
if ( dragIndex < hoverIndex && hoverClientY < hoverMiddleY ) {
return;
}
if ( dragIndex > hoverIndex && hoverClientY > hoverMiddleY ) {
return;
}
moveItem( dragIndex, hoverIndex, id );
item.index = hoverIndex;
},
drop( item ) {
speak( sprintf( i18n.endDrop, id, item.index ) );
},
} );
if ( itemDraggable && isDraggable && isSortable ) {
drag( drop( ref ) );
} else if ( showDragHandle && isDraggable && isSortable ) {
drag( dragHandleRef );
preview( drop( ref ) );
}
const renderControls = () => {
if ( ( ! showArrows && ( ! showDragHandle || ! isDraggable ) ) || ! isSortable ) {
return null;
}
const dragHandleProps = {
size: inlineButtonSize,
type: style === 'minimal' ? 'icon-grey' : 'icon-white',
icon: 'drag-indicator',
iconPrefix: 'gravity-component-icon',
label: i18n.dragLabel,
customAttributes: {
type: 'button',
onKeyDown: handleKeyboardNav,
},
onClick: ( e ) => toggleDragging( e ),
customClasses: classnames( [
'gform-repeater-item__control',
'gform-repeater-item__control--drag-toggle',
], dragHandleClasses ),
ref: showDragHandle && ! itemDraggable ? dragHandleRef : undefined,
disabled: itemCount === 1,
...dragHandleAttributes,
};
const upButtonProps = {
size: inlineButtonSize,
type: style === 'minimal' ? 'icon-grey' : 'icon-white',
label: sprintf( i18n.upLabel, id ),
iconPrefix: 'gravity-component-icon',
icon: 'chevron-up',
onClick: ( e ) => handleArrowPress( e, UP ),
customAttributes: {
type: 'button',
},
customClasses: classnames( [
'gform-repeater-item__control',
'gform-repeater-item__control--up',
], upButtonClasses ),
disabled: itemCount === 1 || ( itemCount !== 1 && index === 0 ),
...upButtonAttributes,
};
const downButtonProps = {
size: inlineButtonSize,
type: style === 'minimal' ? 'icon-grey' : 'icon-white',
label: sprintf( i18n.downLabel, id ),
iconPrefix: 'gravity-component-icon',
icon: 'chevron-down',
onClick: ( e ) => handleArrowPress( e, DOWN ),
customAttributes: {
type: 'button',
},
customClasses: classnames( [
'gform-repeater-item__control',
'gform-repeater-item__control--down',
], downButtonClasses ),
disabled: itemCount === 1 || ( itemCount !== 1 && index === itemCount - 1 ),
...downButtonAttributes,
};
const className = classnames( [
'gform-repeater-item__controls',
] );
return (
<div className={ className }>
{ showDragHandle && isDraggable && isSortable && <Button { ...dragHandleProps } /> }
{ showArrows && isSortable && <Button { ...upButtonProps } /> }
{ showArrows && isSortable && <Button { ...downButtonProps } /> }
</div>
);
};
const itemWrapperProps = {
className: 'gform-repeater-item__wrapper',
};
const blockHeaderProps = {
className: classnames( [
'gform-repeater-item__block-header',
], blockHeaderClasses ),
size: 'text-sm',
weight: 'medium',
...blockHeaderAttributes,
};
let buttonType = 'white';
if ( type === INLINE && style === 'minimal' ) {
buttonType = 'icon-grey';
} else if ( type === INLINE ) {
buttonType = 'icon-white';
}
const renderActionButtons = () => {
return actionButtons.length ? actionButtons.map( ( button, idx ) => {
const { icon, label, onClick, ...rest } = button;
return (
<Button
key={ `action-button-${ idx }` }
size={ inlineButtonSize }
type={ buttonType }
icon={ icon }
iconPosition="leading"
iconPrefix="gravity-component-icon"
label={ label }
onClick={ ( event ) => onClick( event, index ) }
customAttributes={ {
type: 'button',
} }
customClasses={ [
'gform-repeater-item__action',
'gform-repeater-item__action-control',
] }
{ ...rest }
/>
);
} ) : null;
};
const deleteButtonProps = {
size: inlineButtonSize,
type: buttonType,
icon: 'trash',
iconPosition: 'leading',
iconPrefix: 'gravity-component-icon',
label: i18n.deleteLabel,
onClick: handleDelete, // Use handleDelete to manage confirmation
disabled: minItems > 0 && itemCount <= minItems,
customAttributes: {
type: 'button',
},
customClasses: classnames( [
'gform-repeater-item__delete',
'gform-repeater-item__action-control',
], deleteButtonClasses ),
...deleteButtonAttributes,
};
const addButtonProps = {
size: inlineButtonSize,
type: buttonType,
icon: 'plus-regular',
iconAttributes: addButtonAttributes?.iconAttributes || {},
iconPosition: 'leading',
iconPrefix: 'gravity-component-icon',
label: i18n.addLabel,
onClick: addItem,
disabled: !! ( maxItems && itemCount >= maxItems ) || addButtonAttributes?.disabled,
customAttributes: {
type: 'button',
},
customClasses: classnames( [
'gform-repeater-item__add',
'gform-repeater-item__action-control',
], addButtonClasses ),
};
const collapsibleButtonProps = {
size: 'size-height-m',
type: 'icon-grey',
iconPrefix: 'gravity-component-icon',
icon: 'chevron-down',
label: i18n.collapsibleLabel,
onClick: ( e ) => handleCollapsibleClick( e ),
customAttributes: {
type: 'button',
'aria-expanded': ! isCollapsed,
'aria-controls': `${ id }-block-content`,
id: `${ id }-collapsible-button`,
},
customClasses: classnames( [
'gform-repeater-item__collapsible',
], collapsibleButtonClasses ),
...collapsibleButtonAttributes,
};
const blockContentProps = {
'aria-hidden': isCollapsed,
'aria-labelledby': `${ id }-collapsible-button`,
className: 'gform-repeater-item__block-content',
id: `${ id }-block-content`,
role: 'region',
};
const componentProps = {
className: classnames( {
'gform-repeater-item': true,
[ `gform-repeater-item--style-${ style }` ]: true,
[ `gform-repeater-item--type-${ type }` ]: type,
'gform-repeater-item--show-actions-on-hover': showActionsOnHover,
'gform-repeater-item--has-arrows': showArrows && isSortable,
'gform-repeater-item--has-drag-handle': showDragHandle && isDraggable && isSortable,
'gform-repeater-item--is-draggable': isDraggable && isSortable,
'gform-repeater-item--is-sortable': isSortable,
'gform-repeater-item--is-collapsed': isCollapsed,
'gform-repeater-item--is-dragging': isDragging,
'gform-repeater-item--fill-content': fillContent,
'gform-repeater-item--disable-item-drag': ! itemDraggable,
'gform-repeater-item--is-keyboard-nav': keyboardDragActive,
...spacerClasses( itemSpacing ),
}, itemClasses ),
'data-index': index,
'data-handler-id': handlerId,
id,
...itemAttributes,
};
return (
<div { ...componentProps } ref={ ref }>
<div { ...itemWrapperProps }>
{ renderControls() }
{ type === BLOCK && blockContentTitle && (
<Text { ...blockHeaderProps }>{ blockContentTitle }</Text>
) }
{ type === INLINE && children }
<ConditionalWrapper
condition={ ( showAdd || showActions || showDelete ) && type === INLINE && style === 'minimal' }
wrapper={ ( ch ) => <div className="gform-repeater-item__minimal-actions">{ ch }</div> }
>
{ showAdd && type === INLINE && <Button { ...addButtonProps } /> }
{ showActions && type === INLINE && renderActionButtons() }
{ showDelete && type === INLINE && <Button { ...deleteButtonProps } /> }
</ConditionalWrapper>
{ collapsible && type === BLOCK && <Button { ...collapsibleButtonProps } /> }
</div>
{ type === BLOCK && (
<div { ...blockContentProps }>
{ children }
<div className="gform-repeater-item__block-content-footer">
{ showActions && renderActionButtons() }
{ showDelete && <Button { ...deleteButtonProps } /> }
</div>
{ showAdd && ! inlineAdd && <Button { ...addButtonProps } /> }
</div>
) }
{ isConfirmingDelete && deleteConfirmationComponent && (
deleteConfirmationComponent( { index, id, onConfirm: handleConfirmDelete } )
) }
</div>
);
};
RepeaterItem.propTypes = {
addItem: PropTypes.func,
actionButtons: PropTypes.array,
blockContentTitle: PropTypes.string,
blockHeaderAttributes: PropTypes.object,
blockHeaderClasses: PropTypes.oneOfType( [
PropTypes.element,
PropTypes.func,
PropTypes.array,
PropTypes.string,
] ),
children: PropTypes.node,
collapseItem: PropTypes.func,
collapsible: PropTypes.bool,
collapsibleButtonAttributes: PropTypes.object,
collapsibleButtonClasses: PropTypes.oneOfType( [
PropTypes.element,
PropTypes.func,
PropTypes.array,
PropTypes.string,
] ),
confirmDelete: PropTypes.bool,
deleteConfirmationComponent: PropTypes.func,
deleteButtonAttributes: PropTypes.object,
deleteButtonClasses: PropTypes.oneOfType( [
PropTypes.element,
PropTypes.func,
PropTypes.array,
PropTypes.string,
] ),
deleteItem: PropTypes.func,
downButtonAttributes: PropTypes.object,
downButtonClasses: PropTypes.oneOfType( [
PropTypes.element,
PropTypes.func,
PropTypes.array,
PropTypes.string,
] ),
dragHandleAttributes: PropTypes.object,
dragHandleClasses: PropTypes.oneOfType( [
PropTypes.element,
PropTypes.func,
PropTypes.array,
PropTypes.string,
] ),
fillContent: PropTypes.bool,
i18n: PropTypes.object,
id: PropTypes.string.isRequired,
index: PropTypes.number.isRequired,
inlineAdd: PropTypes.bool,
isCollapsed: PropTypes.bool,
isDraggable: PropTypes.bool,
isSortable: PropTypes.bool,
itemAttributes: PropTypes.object,
itemClasses: PropTypes.oneOfType( [
PropTypes.element,
PropTypes.func,
PropTypes.array,
PropTypes.string,
] ),
itemCount: PropTypes.number,
itemDraggable: PropTypes.bool,
itemSpacing: PropTypes.oneOfType( [
PropTypes.string,
PropTypes.number,
PropTypes.array,
PropTypes.object,
] ),
maxItems: PropTypes.number,
minItems: PropTypes.number,
moveItem: PropTypes.func.isRequired,
repeaterInstanceId: PropTypes.string,
showAdd: PropTypes.bool,
showActions: PropTypes.bool,
showActionsOnHover: PropTypes.bool,
showArrows: PropTypes.bool,
showDelete: PropTypes.bool,
showDragHandle: PropTypes.bool,
speak: PropTypes.func,
style: PropTypes.string,
type: PropTypes.string,
upButtonAttributes: PropTypes.object,
upButtonClasses: PropTypes.oneOfType( [
PropTypes.string,
PropTypes.array,
PropTypes.object,
] ),
};
export default RepeaterItem;