@plone/volto
Version:
Volto
268 lines (254 loc) • 9.04 kB
JSX
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" />
{/* 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;