UNPKG

@gravityforms/components

Version:

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

392 lines (355 loc) 10.5 kB
import { React, ReactDND, PropTypes, classnames } from '@gravityforms/libraries'; 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 = ( { blockContentTitle = '', blockHeaderAttributes = {}, blockHeaderClasses = [], children = null, collapseItem = () => {}, collapsible = false, collapsibleButtonAttributes = {}, collapsibleButtonClasses = [], deleteButtonAttributes = {}, deleteButtonClasses = [], deleteItem = () => {}, downButtonAttributes = {}, downButtonClasses = [], dragHandleAttributes = {}, dragHandleClasses = [], fillContent = false, i18n = {}, id = '', index = 0, isCollapsed = false, isDraggable = false, isSortable = false, itemAttributes = {}, itemClasses = [], itemCount = 0, itemDraggable = false, itemSpacing = '', minItems = 0, moveItem = () => {}, showArrows = false, showDragHandle = false, showDelete = false, speak = () => {}, type = INLINE, upButtonAttributes = {}, upButtonClasses = [], } ) => { const ref = useRef( null ); const dragHandleRef = useRef( null ); const startIndexRef = useRef( null ); const [ keyboardDragActive, setKeyboardDragActive ] = useState( false ); 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 [ { isDragging }, drag, preview ] = useDrag( { type: itemTypes.REPEATER_ITEM, item: { id, index }, collect: ( monitor ) => ( { isDragging: monitor.isDragging(), } ), } ); const [ { handlerId }, drop ] = useDrop( { accept: itemTypes.REPEATER_ITEM, collect( monitor ) { return { handlerId: monitor.getHandlerId(), isOver: monitor.isOver(), }; }, hover( item, monitor ) { 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: 'size-height-m', type: 'icon-white', icon: 'drag-indicator', iconPrefix: 'gravity-component-icon', label: i18n.dragLabel, customAttributes: { 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: 'size-height-m', type: 'icon-white', label: sprintf( i18n.upLabel, id ), iconPrefix: 'gravity-component-icon', icon: 'chevron-up', onClick: ( e ) => handleArrowPress( e, UP ), customClasses: classnames( [ 'gform-repeater-item__control', 'gform-repeater-item__control--up', ], upButtonClasses ), disabled: itemCount === 1 || ( itemCount !== 1 && index === 0 ), ...upButtonAttributes, }; const downButtonProps = { size: 'size-height-m', type: 'icon-white', label: sprintf( i18n.downLabel, id ), iconPrefix: 'gravity-component-icon', icon: 'chevron-down', onClick: ( e ) => handleArrowPress( e, DOWN ), 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, }; const deleteButtonProps = { size: 'size-height-m', type: type === INLINE ? 'icon-white' : 'white', icon: 'trash', iconPosition: 'leading', label: i18n.deleteLabel, onClick: () => deleteItem( index, id ), disabled: minItems && itemCount <= minItems, customClasses: classnames( [ 'gform-repeater-item__delete', ], deleteButtonClasses ), ...deleteButtonAttributes, }; const collapsibleButtonProps = { size: 'size-height-m', type: 'unstyled', iconPrefix: 'gravity-component-icon', icon: 'chevron-down', onClick: ( e ) => handleCollapsibleClick( e ), customClasses: classnames( [ 'gform-repeater-item__collapsible', ], collapsibleButtonClasses ), ...collapsibleButtonAttributes, }; const componentProps = { className: classnames( { 'gform-repeater-item': true, [ `gform-repeater-item--type-${ type }` ]: type, '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 } { showDelete && type === INLINE && <Button { ...deleteButtonProps } /> } { collapsible && type === BLOCK && <Button { ...collapsibleButtonProps } /> } </div> { type === BLOCK && ( <div className="gform-repeater-item__block-content"> { children } { showDelete && <Button { ...deleteButtonProps } /> } </div> ) } </div> ); }; RepeaterItem.propTypes = { 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, ] ), 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, 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, ] ), minItems: PropTypes.number, moveItem: PropTypes.func.isRequired, showArrows: PropTypes.bool, showDragHandle: PropTypes.bool, showDelete: PropTypes.bool, speak: PropTypes.func, type: PropTypes.string, upButtonAttributes: PropTypes.object, upButtonClasses: PropTypes.oneOfType( [ PropTypes.element, PropTypes.func, PropTypes.array, PropTypes.string, ] ), }; export default RepeaterItem;