UNPKG

@plone/volto

Version:
268 lines (254 loc) 9.04 kB
import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { Accordion, Button, Segment } from 'semantic-ui-react'; import DragDropList from '@plone/volto/components/manage/DragDropList/DragDropList'; import Icon from '@plone/volto/components/theme/Icon/Icon'; import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWrapper'; import { applySchemaDefaults } from '@plone/volto/helpers/Blocks/Blocks'; import { reorderArray } from '@plone/volto/helpers/Utils/Utils'; import ObjectWidget from '@plone/volto/components/manage/Widgets/ObjectWidget'; import upSVG from '@plone/volto/icons/up-key.svg'; import downSVG from '@plone/volto/icons/down-key.svg'; import deleteSVG from '@plone/volto/icons/delete.svg'; import addSVG from '@plone/volto/icons/add.svg'; import dragSVG from '@plone/volto/icons/drag.svg'; import { v4 as uuid } from 'uuid'; import config from '@plone/volto/registry'; const messages = defineMessages({ labelRemoveItem: { id: 'Remove item', defaultMessage: 'Remove item', }, labelCollapseItem: { id: 'Collapse item', defaultMessage: 'Collapse item', }, labelShowItem: { id: 'Show item', defaultMessage: 'Show item', }, emptyObjectList: { id: 'Empty object list', defaultMessage: 'Empty object list', }, add: { id: 'Add (object list)', defaultMessage: 'Add', }, }); /** * This is a DataGridField-equivalent widget for schema-based values. * The shape of the items in the array is defined using a schema. * * ObjectListWidget can receive an optional `schemaExtender` prop which is * a function that can mutate the schema for each individual item in the array. * An example schema definition of the a field that renders with the * ObjectListWidget: * * ```jsx * columns: { * title: 'Columns', * description: 'Leave empty to show all columns', * schema: SomeItemSchema, * widget: 'object_list', * schemaExtender: (schema, data) => { * const mutated = lodash.cloneDeep(schema); * mutated.properties.extraField = { * title: 'Extra field', * } * mutated.fieldsets[0].fields.push('extraField'); * return mutated; * }, * activeObject: 0, // Current active object drilled down from the schema (if present) * setActiveObject: () => {} // The current active object state updater function drilled down from the schema (if present) * }, * ``` */ const ObjectListWidget = (props) => { const { block, fieldSet, id, value = [], onChange, schemaExtender, schemaName, } = props; // This allows to use a `schemaName` prop defined as source of the schema // if not present, it will use the schema defined in the schema prop const schema = config.getUtility({ type: 'schema', name: schemaName, }).method || props.schema; const [localActiveObject, setLocalActiveObject] = React.useState( props.activeObject ?? value.length - 1, ); let activeObject, setActiveObject; if ( (props.activeObject || props.activeObject === 0) && props.setActiveObject ) { activeObject = props.activeObject; setActiveObject = props.setActiveObject; } else { activeObject = localActiveObject; setActiveObject = setLocalActiveObject; } const intl = useIntl(); function handleChangeActiveObject(e, blockProps) { const { index } = blockProps; const newIndex = activeObject === index ? -1 : index; setActiveObject(newIndex); } const objectSchema = typeof schema === 'function' ? schema(props) : schema; const topLayerShadow = '0 1px 1px rgba(0,0,0,0.15)'; const secondLayer = ', 0 10px 0 -5px #eee, 0 10px 1px -4px rgba(0,0,0,0.15)'; const thirdLayer = ', 0 20px 0 -10px #eee, 0 20px 1px -9px rgba(0,0,0,0.15)'; return ( <div className="objectlist-widget"> <FormFieldWrapper {...props} noForInFieldLabel className="objectlist"> <div className="add-item-button-wrapper"> <Button compact icon aria-label={ objectSchema.addMessage || `${intl.formatMessage(messages.add)} ${objectSchema.title}` } onClick={(e) => { e.preventDefault(); const data = { '@id': uuid(), }; const objSchema = schemaExtender ? schemaExtender(schema, data, intl) : objectSchema; const dataWithDefaults = applySchemaDefaults({ data, schema: objSchema, intl, }); onChange(id, [...value, dataWithDefaults]); setActiveObject(value.length); }} > <Icon name={addSVG} size="18px" /> &nbsp; {/* Custom addMessage in schema, else default to English */} {objectSchema.addMessage || `${intl.formatMessage(messages.add)} ${objectSchema.title}`} </Button> </div> {value.length === 0 && ( <input aria-labelledby={`fieldset-${ fieldSet || 'default' }-field-label-${id}`} type="hidden" value={intl.formatMessage(messages.emptyObjectList)} /> )} </FormFieldWrapper> <DragDropList style={{ boxShadow: `${topLayerShadow}${value.length > 1 ? secondLayer : ''}${ value.length > 2 ? thirdLayer : '' }`, }} forwardedAriaLabelledBy={`fieldset-${ fieldSet || 'default' }-field-label-${id}`} childList={value.map((o) => [o['@id'], o])} onMoveItem={(result) => { const { source, destination } = result; if (!destination) { return; } const newValue = reorderArray(value, source.index, destination.index); onChange(id, newValue); return true; }} > {({ child, childId, index, draginfo }) => { return ( <div ref={draginfo.innerRef} {...draginfo.draggableProps} key={childId} > <Accordion key={index} fluid styled> <Accordion.Title active={activeObject === index} index={index} onClick={handleChangeActiveObject} aria-label={`${ activeObject === index ? intl.formatMessage(messages.labelCollapseItem) : intl.formatMessage(messages.labelShowItem) } #${index + 1}`} > <button style={{ visibility: 'visible', display: 'inline-block', }} {...draginfo.dragHandleProps} className="drag handle" > <Icon name={dragSVG} size="18px" /> </button> <div className="accordion-title-wrapper"> {`${objectSchema.title} #${index + 1}`} </div> <div className="accordion-tools"> <button aria-label={`${intl.formatMessage( messages.labelRemoveItem, )} #${index + 1}`} onClick={() => { onChange( id, value.filter((v, i) => i !== index), ); }} > <Icon name={deleteSVG} size="20px" color="#e40166" /> </button> {activeObject === index ? ( <Icon name={upSVG} size="20px" /> ) : ( <Icon name={downSVG} size="20px" /> )} </div> </Accordion.Title> <Accordion.Content active={activeObject === index}> <Segment> <ObjectWidget id={`${id}-${index}`} key={`ow-${id}-${index}`} block={block} schema={ schemaExtender ? schemaExtender(schema, child, intl) : objectSchema } value={child} onChange={(fi, fv) => { const newvalue = value.map((v, i) => i !== index ? v : fv, ); onChange(id, newvalue); }} /> </Segment> </Accordion.Content> </Accordion> </div> ); }} </DragDropList> </div> ); }; export default ObjectListWidget;