@gravityforms/components
Version:
UI components for use in Gravity Forms development. Both React and vanilla js flavors.
392 lines (355 loc) • 10.5 kB
JavaScript
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;