UNPKG

@gravityforms/components

Version:

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

462 lines (435 loc) 14.2 kB
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;