UNPKG

@gravityforms/components

Version:

UI components for use in Gravity Forms development. Both React and vanilla js flavors.

535 lines (489 loc) 14.9 kB
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;