@gravityforms/components
Version:
UI components for use in Gravity Forms development. Both React and vanilla js flavors.
462 lines (435 loc) • 14.2 kB
JavaScript
import { React, ReactDND, ReactDNDHtml5Backend, PropTypes, classnames } from '@gravityforms/libraries';
import { spacerClasses, sprintf } from '@gravityforms/utils';
import itemTypes from './item-types';
import KanbanLoader from './KanbanLoader';
import Box from '../../elements/Box';
import Button from '../../elements/Button';
import Grid from '../../elements/Grid';
import Input from '../../elements/Input';
import StatusIndicator from '../../elements/StatusIndicator';
import Text from '../../elements/Text';
import Droplist from '../Droplist';
import Overlay from '../Overlay';
import Swatch from '../Swatch';
import { GRAY, STATUSES, SWATCH_PALETTE, STATUS_SWATCH_MAP } from './swatch';
const { useCallback, useEffect, useRef, useState } = React;
const { useDrag, useDrop } = ReactDND;
const { getEmptyImage } = ReactDNDHtml5Backend;
const NEEDS_I18N_LABEL = 'Needs i18n';
/**
* @module KanbanColumn
* @description A column in the Kanban board.
*
* @since 5.8.4
*
* @param {object} props The props for the Kanban column.
* @param {boolean} props.cardHoveredWhenEmpty Whether the card is hovered when empty.
* @param {JSX.Element} props.children The children elements to render inside the column.
* @param {object} props.columnCountAttributes Attributes for the column count.
* @param {string|Array|object} props.columnCountClasses Classes for the column count.
* @param {object} props.columnData Data for the column.
* @param {object} props.columnLabelAttributes Attributes for the column label.
* @param {string|Array|object} props.columnLabelClasses Classes for the column label.
* @param {object} props.customAttributes Custom attributes for the column.
* @param {string|Array|object} props.customClasses Custom classes for the column.
* @param {boolean} props.deleteDisabled Whether deleting the column is disabled.
* @param {object} props.dragHandleAttributes Attributes for the drag handle.
* @param {string|Array|object} props.dragHandleClasses Classes for the drag handle.
* @param {boolean} props.hoveredLeft Whether the column is hovered on the left.
* @param {boolean} props.hoveredRight Whether the column is hovered on the right.
* @param {object} props.i18n I18n strings for the column.
* @param {string} props.id The ID of the column.
* @param {number} props.index The index of the column.
* @param {boolean} props.isDragLayer Whether this is being rendered in a drag layer.
* @param {boolean} props.isFirstColumn Whether this is the first column.
* @param {boolean} props.isLastColumn Whether this is the last column.
* @param {string} props.kanbanId The ID of the Kanban board.
* @param {Function} props.onDelete Callback when the column is deleted.
* @param {Function} props.moveColumn Function to move the column.
* @param {Function} props.onDragEnd Callback when dragging column ends.
* @param {boolean} props.isLoading Whether the column is in a loading state.
* @param {Function} props.onDrop Callback when a card is dropped on the column.
* @param {Function} props.onHover Callback when a card is hovering over the column.
* @param {string|number|Array|object} props.spacing Spacing for the column.
* @param {Function} props.updateColumnLabel Function to update the column label.
*
* @return {JSX.Element} The Kanban column component.
*/
const KanbanColumn = ( {
cardHoveredWhenEmpty = false,
children = null,
columnCountAttributes = {},
columnCountClasses = [],
columnData = {},
columnLabelAttributes = {},
columnLabelClasses = [],
customAttributes = {},
customClasses = [],
deleteDisabled = false,
dragHandleAttributes = {},
dragHandleClasses = [],
hoveredLeft = false,
hoveredRight = false,
i18n = {},
id = '',
index = 0,
isDragLayer = false,
isFirstColumn = false,
isLastColumn = false,
isLoading = false,
kanbanId = '',
moveColumn = () => {},
onDelete = () => {},
onDragEnd = () => {},
onDrop = () => {},
onHover = () => {},
spacing = '',
updateColumnLabel = () => {},
} ) => {
const columnItemType = `${ itemTypes.KANBAN_COLUMN }_${ kanbanId }`;
const cardItemType = `${ itemTypes.KANBAN_CARD }_${ kanbanId }`;
const [ overlayOpen, setOverlayOpen ] = useState( false );
const [ columnLabel, setColumnLabel ] = useState( columnLabelAttributes.label || '' );
const [ labelPillStyle, setLabelPillStyle ] = useState( STATUSES.includes( columnLabelAttributes.status?.toLowerCase() ) ? columnLabelAttributes.status?.toLowerCase() : GRAY );
const columnRef = useRef( null );
const [ { isDragging }, drag, preview ] = useDrag( {
type: columnItemType,
item: () => {
const item = {
id,
index,
column: columnData,
};
if ( columnRef.current ) {
item.rect = columnRef.current.getBoundingClientRect();
}
return item;
},
collect: ( monitor ) => ( {
isDragging: monitor.isDragging(),
} ),
end: () => {
onDragEnd();
},
} );
const [ , drop ] = useDrop( {
accept: cardItemType,
hover: ( item, monitor ) => {
onHover( item, monitor, id );
},
drop: ( item ) => {
onDrop( item );
},
} );
useEffect( () => {
preview( getEmptyImage(), { captureDraggingState: true } );
}, [] ); // eslint-disable-line react-hooks/exhaustive-deps
const getColumnGridItemProps = ( type = '' ) => ( {
customClasses: [ 'gform-kanban__column-grid-item', `gform-kanban__column-grid-item--${ type }` ],
elementType: 'div',
item: true,
spacing: type !== 'cards' ? 3 : 0,
} );
const componentProps = {
...customAttributes,
className: classnames( {
'gform-kanban__column': true,
'gform-kanban__column--is-dragging': isDragging && ! isDragLayer,
'gform-kanban__column--drag-layer': isDragLayer,
'gform-kanban__column--drop-left': hoveredLeft,
'gform-kanban__column--drop-right': hoveredRight,
'gform-kanban__column--card-drop-when-empty': cardHoveredWhenEmpty,
'gform-kanban__column--loading': isLoading,
...spacerClasses( spacing ),
}, customClasses ),
'data-index': index,
id,
};
const columnGridProps = {
alignItems: 'stretch',
container: true,
customClasses: [ 'gform-kanban__column-grid' ],
direction: 'column',
elementType: 'div',
justifyContent: 'flex-start',
};
const headerProps = {
customClasses: [ 'gform-kanban__column-header' ],
display: isLoading ? 'block' : 'flex',
};
const headerLabelProps = {
customClasses: [ 'gform-kanban__column-header-label' ],
display: isLoading ? 'block' : 'flex',
};
const headerActionsProps = {
customClasses: [ 'gform-kanban__column-header-actions' ],
display: 'flex',
};
const columnLabelProps = {
hasDot: false,
isStatic: false,
status: columnLabelAttributes.status || GRAY,
...columnLabelAttributes,
customAttributes: {
...( columnLabelAttributes?.customAttributes || {} ),
onClick: () => setOverlayOpen( true ),
},
customClasses: classnames( {
'gform-kanban__column-label': true,
}, columnLabelClasses ),
};
const columnCountProps = {
hasDot: false,
status: GRAY,
spacing: [ 0, 0, 0, 1 ],
...columnCountAttributes,
customClasses: classnames( {
'gform-kanban__column-count': true,
}, columnCountClasses ),
};
const droplistProps = {
align: 'right',
closeOnClick: true,
id: `${ id }-droplist`,
listItems: [
{
key: `${ id }-move-left`,
props: {
element: 'button',
id: `${ id }-move-left`,
label: i18n?.moveLeft || NEEDS_I18N_LABEL,
iconBefore: 'arrow-narrow-left',
customAttributes: {
disabled: isFirstColumn,
onClick: () => {
if ( isFirstColumn ) {
return;
}
moveColumn( index, index - 1, id );
},
},
},
},
{
key: `${ id }-move-right`,
props: {
element: 'button',
id: `${ id }-move-right`,
label: i18n?.moveRight || NEEDS_I18N_LABEL,
iconBefore: 'arrow-narrow-right',
customAttributes: {
disabled: isLastColumn,
onClick: () => {
if ( isLastColumn ) {
return;
}
moveColumn( index, index + 1, id );
},
},
},
},
{
key: `${ id }-delete-column`,
props: {
element: 'button',
id: `${ id }-delete-column`,
label: i18n?.deleteColumn || NEEDS_I18N_LABEL,
iconBefore: 'trash',
style: 'error',
customAttributes: {
disabled: deleteDisabled,
onClick: () => {
if ( deleteDisabled ) {
return;
}
onDelete( id );
},
},
},
},
],
triggerAttributes: {
icon: 'dots-horizontal',
iconPrefix: 'gravity-component-icon',
size: 'size-height-s',
title: i18n?.columnActionsTrigger ? sprintf( i18n.columnActionsTrigger, columnLabelAttributes?.label || '' ) : NEEDS_I18N_LABEL,
type: 'icon-white',
},
};
const dragHandleProps = {
size: 'size-height-m',
type: 'icon-grey',
icon: 'drag-indicator',
iconPrefix: 'gravity-component-icon',
spacing: [ 0, 0, 0, 1 ],
...dragHandleAttributes,
customClasses: classnames( {
'gform-kanban__column-drag-handle': true,
}, dragHandleClasses ),
};
const overlayProps = {
confirmButtonAttributes: {
disabled: ! columnLabel.trim(),
},
customClasses: [ 'gform-kanban__column-overlay' ],
hasConfirm: true,
i18n: {
cancel: i18n?.overlayCancel || NEEDS_I18N_LABEL,
confirm: i18n?.overlayConfirm || NEEDS_I18N_LABEL,
},
isOpen: overlayOpen,
onConfirm: () => {
setOverlayOpen( false );
updateColumnLabel( id, { label: columnLabel, status: labelPillStyle } );
},
onCancel: () => {
setOverlayOpen( false );
setTimeout( () => {
setColumnLabel( columnLabelAttributes.label || '' );
setLabelPillStyle( STATUSES.includes( columnLabelAttributes.status?.toLowerCase() ) ? columnLabelAttributes.status?.toLowerCase() : GRAY );
}, 250 );
},
onClose: () => {
setOverlayOpen( false );
},
};
const overlayInputProps = {
controlled: true,
id: `${ id }-overlay-input`,
name: `${ id }-overlay-input`,
labelAttributes: {
label: i18n?.overlayInputLabel || NEEDS_I18N_LABEL,
size: 'text-sm',
weight: 'medium',
},
onChange: ( value ) => {
setColumnLabel( value );
},
spacing: 3,
value: columnLabel,
width: 'full',
};
const overlaySwatchLabelProps = {
customAttributes: {
id: `${ id }-overlay-swatch-label`,
},
content: i18n?.overlaySwatchLabel || NEEDS_I18N_LABEL,
size: 'text-sm',
weight: 'medium',
};
const overlaySwatchProps = {
allowNew: false,
controlled: true,
customAttributes: {
'aria-labelledby': `${ id }-overlay-swatch-label`,
},
customClasses: [ 'gform-kanban__column-overlay-swatch' ],
id: `${ id }-overlay-swatch`,
name: `${ id }-overlay-swatch`,
onChange: ( value ) => {
const status = Object.keys( STATUS_SWATCH_MAP ).find( ( key ) => STATUS_SWATCH_MAP[ key ] === value );
if ( status ) {
setLabelPillStyle( status );
}
},
palette: SWATCH_PALETTE,
value: STATUS_SWATCH_MAP[ labelPillStyle ],
};
const setRefs = useCallback( ( node ) => {
columnRef.current = node;
drop( node );
}, [ drop ] );
return (
<div { ...componentProps } ref={ setRefs }>
<Grid { ...columnGridProps }>
<Grid { ...getColumnGridItemProps( 'header' ) }>
<Box { ...headerProps }>
<Box { ...headerLabelProps }>
{
isLoading
? <KanbanLoader />
: <>
<StatusIndicator { ...columnLabelProps } />
<StatusIndicator { ...columnCountProps } />
</>
}
</Box>
{ ! isLoading && (
<>
<Box { ...headerActionsProps }>
<Droplist { ...droplistProps } />
<Button { ...dragHandleProps } ref={ drag } />
</Box>
{ ! columnLabelProps.isStatic && overlayOpen && (
<Overlay { ...overlayProps }>
<Input { ...overlayInputProps } />
<Text { ...overlaySwatchLabelProps } />
<Swatch { ...overlaySwatchProps } />
</Overlay>
) }
</>
) }
</Box>
</Grid>
<Grid { ...getColumnGridItemProps( 'cards' ) }>
{ children }
</Grid>
</Grid>
</div>
);
};
KanbanColumn.propTypes = {
cardHoveredWhenEmpty: PropTypes.bool,
children: PropTypes.oneOfType( [
PropTypes.arrayOf( PropTypes.node ),
PropTypes.node,
] ),
columnCountAttributes: PropTypes.object,
columnCountClasses: PropTypes.oneOfType( [
PropTypes.string,
PropTypes.array,
PropTypes.object,
] ),
columnLabelAttributes: PropTypes.object,
columnLabelClasses: PropTypes.oneOfType( [
PropTypes.string,
PropTypes.array,
PropTypes.object,
] ),
customAttributes: PropTypes.object,
customClasses: PropTypes.oneOfType( [
PropTypes.string,
PropTypes.array,
PropTypes.object,
] ),
deleteDisabled: PropTypes.bool,
dragHandleAttributes: PropTypes.object,
dragHandleClasses: PropTypes.oneOfType( [
PropTypes.string,
PropTypes.array,
PropTypes.object,
] ),
hoveredLeft: PropTypes.bool,
hoveredRight: PropTypes.bool,
i18n: PropTypes.object,
id: PropTypes.string,
index: PropTypes.number,
isFirstColumn: PropTypes.bool,
isLastColumn: PropTypes.bool,
isLoading: PropTypes.bool,
kanbanId: PropTypes.string,
moveColumn: PropTypes.func,
onDelete: PropTypes.func,
onDragEnd: PropTypes.func,
onDrop: PropTypes.func,
onHover: PropTypes.func,
spacing: PropTypes.oneOfType( [
PropTypes.string,
PropTypes.number,
PropTypes.array,
PropTypes.object,
] ),
updateColumnLabel: PropTypes.func,
};
KanbanColumn.displayName = 'KanbanColumn';
export default KanbanColumn;