migrate object and list widgets
This commit is contained in:
@ -8,13 +8,13 @@ import { DateControl, DatePreview } from 'netlify-cms-widget-date';
|
||||
import { DateTimeControl, DateTimePreview } from 'netlify-cms-widget-datetime';
|
||||
import { FileControl, FilePreview } from 'netlify-cms-widget-file';
|
||||
import { ImageControl, ImagePreview } from 'netlify-cms-widget-image';
|
||||
import { ListControl, ListPreview } from 'netlify-cms-widget-list';
|
||||
import { ObjectControl, ObjectPreview } from 'netlify-cms-widget-object';
|
||||
import { StringControl, StringPreview } from 'netlify-cms-widget-string';
|
||||
// import { NumberControl, NumberPreview } from 'netlify-cms-widget-number';
|
||||
// import { TextControl, TextPreview } from 'netlify-cms-widget-text';
|
||||
// import { SelectControl, SelectPreview } from 'netlify-cms-widget-select';
|
||||
// import { MarkdownControl, MarkdownPreview } from 'netlify-cms-widget-markdown';
|
||||
// import { ListControl, ListPreview } from 'netlify-cms-widget-list';
|
||||
// import { ObjectControl, ObjectPreview } from 'netlify-cms-widget-object';
|
||||
// import { RelationControl, RelationPreview } from 'netlify-cms-widget-relation';
|
||||
import image from 'netlify-cms-editor-component-image';
|
||||
|
||||
@ -27,12 +27,12 @@ registerWidget('date', DateControl, DatePreview);
|
||||
registerWidget('datetime', DateTimeControl, DateTimePreview);
|
||||
registerWidget('file', FileControl, FilePreview);
|
||||
registerWidget('image', ImageControl, ImagePreview);
|
||||
registerWidget('list', ListControl, ListPreview);
|
||||
registerWidget('object', ObjectControl, ObjectPreview);
|
||||
registerWidget('string', StringControl, StringPreview);
|
||||
// registerWidget('text', TextControl, TextPreview);
|
||||
// registerWidget('number', NumberControl, NumberPreview);
|
||||
// registerWidget('list', ListControl, ListPreview);
|
||||
// registerWidget('markdown', MarkdownControl, MarkdownPreview);
|
||||
// registerWidget('select', SelectControl, SelectPreview);
|
||||
// registerWidget('object', ObjectControl, ObjectPreview);
|
||||
// registerWidget('relation', RelationControl, RelationPreview);
|
||||
registerEditorComponent(image);
|
||||
|
@ -172,6 +172,7 @@ export default class EditorControl extends React.Component {
|
||||
hasActiveStyle={this.state.styleActive}
|
||||
setActiveStyle={() => this.setState({ styleActive: true })}
|
||||
setInactiveStyle={() => this.setState({ styleActive: false })}
|
||||
resolveWidget={resolveWidget}
|
||||
ref={processControlRef && partial(processControlRef, fieldName)}
|
||||
editorControl={EditorControl}
|
||||
/>
|
||||
|
@ -39,6 +39,7 @@ export default class Widget extends Component {
|
||||
onAddAsset: PropTypes.func.isRequired,
|
||||
onRemoveInsertedMedia: PropTypes.func.isRequired,
|
||||
getAsset: PropTypes.func.isRequired,
|
||||
resolveWidget: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
@ -189,7 +190,8 @@ export default class Widget extends Component {
|
||||
setInactiveStyle,
|
||||
hasActiveStyle,
|
||||
editorControl,
|
||||
uniqueFieldId
|
||||
uniqueFieldId,
|
||||
resolveWidget,
|
||||
} = this.props;
|
||||
return React.createElement(controlComponent, {
|
||||
field,
|
||||
@ -213,6 +215,7 @@ export default class Widget extends Component {
|
||||
setInactiveStyle,
|
||||
hasActiveStyle,
|
||||
editorControl,
|
||||
resolveWidget,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
.nc-fileControl-input {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.nc-fileControl-imageUpload {
|
||||
cursor: pointer;
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import withMediaControl from 'EditorWidgets/withMedia/withMediaControl';
|
||||
|
||||
const FileControl = withMediaControl();
|
||||
|
||||
export default FileControl;
|
@ -1,15 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
export default function FilePreview({ value, getAsset }) {
|
||||
return (<div className="nc-widgetPreview">
|
||||
{ value ?
|
||||
<a href={getAsset(value)}>{ value }</a>
|
||||
: null}
|
||||
</div>);
|
||||
}
|
||||
|
||||
FilePreview.propTypes = {
|
||||
getAsset: PropTypes.func.isRequired,
|
||||
value: PropTypes.node,
|
||||
};
|
@ -1,4 +0,0 @@
|
||||
.nc-imagePreview-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import withMediaControl from 'EditorWidgets/withMedia/withMediaControl';
|
||||
|
||||
const ImageControl = withMediaControl(true);
|
||||
|
||||
export default ImageControl;
|
@ -1,19 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
export default function ImagePreview({ value, getAsset }) {
|
||||
return (<div className='nc-widgetPreview'>
|
||||
{ value ?
|
||||
<img
|
||||
src={getAsset(value)}
|
||||
className='nc-imageWidget-image'
|
||||
role="presentation"
|
||||
/>
|
||||
: null}
|
||||
</div>);
|
||||
}
|
||||
|
||||
ImagePreview.propTypes = {
|
||||
getAsset: PropTypes.func.isRequired,
|
||||
value: PropTypes.node,
|
||||
};
|
@ -1,88 +0,0 @@
|
||||
.nc-listControl {
|
||||
padding: 0 14px 14px;
|
||||
|
||||
&.nc-listControl-collapsed {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.list-item-dragging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.nc-listControl-topBar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 0 -14px;
|
||||
background-color: var(--textFieldBorderColor);
|
||||
padding: 13px;
|
||||
}
|
||||
|
||||
.nc-listControl-addButton {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 2px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
border-radius: 3px;
|
||||
|
||||
& .nc-icon {
|
||||
padding-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.nc-listControl-listCollapseToggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.nc-listControl-listCollapseToggleButton{
|
||||
padding: 4px;
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
|
||||
&:last-of-type {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.nc-listControl-item {
|
||||
margin-top: 18px;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
.nc-listControl-itemTopBar {
|
||||
background-color: var(--textFieldBorderColor);
|
||||
}
|
||||
|
||||
.nc-listControl-objectLabel {
|
||||
display: none;
|
||||
border-top: 0;
|
||||
background-color: var(--textFieldBorderColor);
|
||||
padding: 13px;
|
||||
border-radius: 0 0 var(--borderRadius) var(--borderRadius);
|
||||
}
|
||||
|
||||
.nc-listControl-objectControl {
|
||||
padding: 6px 14px 14px;
|
||||
border-top: 0;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.nc-listControl-collapsed {
|
||||
& .nc-listControl-objectLabel {
|
||||
display: block;
|
||||
}
|
||||
& .nc-listControl-objectControl {
|
||||
display: none;
|
||||
}
|
||||
}
|
@ -1,307 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { List, Map } from 'immutable';
|
||||
import { partial } from 'lodash';
|
||||
import c from 'classnames';
|
||||
import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc';
|
||||
import { Icon, ListItemTopBar } from 'netlify-cms-ui-default';
|
||||
import ObjectControl from 'EditorWidgets/Object/ObjectControl';
|
||||
|
||||
function ListItem(props) {
|
||||
return <div {...props} className={`list-item ${ props.className || '' }`}>{props.children}</div>;
|
||||
}
|
||||
ListItem.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
ListItem.displayName = 'list-item';
|
||||
|
||||
function valueToString(value) {
|
||||
return value ? value.join(',').replace(/,([^\s]|$)/g, ', $1') : '';
|
||||
}
|
||||
|
||||
const SortableListItem = SortableElement(ListItem);
|
||||
|
||||
const TopBar = ({ allowAdd, onAdd, listLabel, onCollapseAllToggle, allItemsCollapsed, itemsCount }) => (
|
||||
<div className="nc-listControl-topBar">
|
||||
<div className="nc-listControl-listCollapseToggle">
|
||||
<button className="nc-listControl-listCollapseToggleButton" onClick={onCollapseAllToggle}>
|
||||
<Icon type="chevron" direction={allItemsCollapsed ? 'right' : 'down'} size="small" />
|
||||
</button>
|
||||
{itemsCount} {listLabel}
|
||||
</div>
|
||||
|
||||
{
|
||||
allowAdd ?
|
||||
<button className="nc-listControl-addButton" onClick={onAdd}>
|
||||
Add {listLabel} <Icon type="add" size="xsmall" />
|
||||
</button>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
const SortableList = SortableContainer(({ items, renderItem }) => {
|
||||
return <div>{items.map(renderItem)}</div>;
|
||||
});
|
||||
|
||||
const valueTypes = {
|
||||
SINGLE: 'SINGLE',
|
||||
MULTIPLE: 'MULTIPLE',
|
||||
};
|
||||
|
||||
export default class ListControl extends Component {
|
||||
static propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onChangeObject: PropTypes.func.isRequired,
|
||||
value: ImmutablePropTypes.list,
|
||||
field: PropTypes.object,
|
||||
forID: PropTypes.string,
|
||||
mediaPaths: ImmutablePropTypes.map.isRequired,
|
||||
getAsset: PropTypes.func.isRequired,
|
||||
onOpenMediaLibrary: PropTypes.func.isRequired,
|
||||
onAddAsset: PropTypes.func.isRequired,
|
||||
onRemoveInsertedMedia: PropTypes.func.isRequired,
|
||||
classNameWrapper: PropTypes.string.isRequired,
|
||||
setActiveStyle: PropTypes.func.isRequired,
|
||||
setInactiveStyle: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
value: List(),
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { field, value } = props;
|
||||
const allItemsCollapsed = field.get('collapsed', true);
|
||||
const itemsCollapsed = value && Array(value.size).fill(allItemsCollapsed);
|
||||
|
||||
this.state = {
|
||||
itemsCollapsed: List(itemsCollapsed),
|
||||
value: valueToString(value),
|
||||
};
|
||||
|
||||
this.valueType = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Always update so that each nested widget has the option to update. This is
|
||||
* required because ControlHOC provides a default `shouldComponentUpdate`
|
||||
* which only updates if the value changes, but every widget must be allowed
|
||||
* to override this.
|
||||
*/
|
||||
shouldComponentUpdate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { field } = this.props;
|
||||
|
||||
if (field.get('fields')) {
|
||||
this.valueType = valueTypes.MULTIPLE;
|
||||
} else if (field.get('field')) {
|
||||
this.valueType = valueTypes.SINGLE;
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps) {
|
||||
if (this.props.field === nextProps.field) return;
|
||||
|
||||
if (nextProps.field.get('fields')) {
|
||||
this.valueType = valueTypes.MULTIPLE;
|
||||
} else if (nextProps.field.get('field')) {
|
||||
this.valueType = valueTypes.SINGLE;
|
||||
}
|
||||
}
|
||||
|
||||
handleChange = (e) => {
|
||||
const { onChange } = this.props;
|
||||
const oldValue = this.state.value;
|
||||
const newValue = e.target.value;
|
||||
const listValue = e.target.value.split(',');
|
||||
if (newValue.match(/,$/) && oldValue.match(/, $/)) {
|
||||
listValue.pop();
|
||||
}
|
||||
|
||||
const parsedValue = valueToString(listValue);
|
||||
this.setState({ value: parsedValue });
|
||||
onChange(listValue.map(val => val.trim()));
|
||||
};
|
||||
|
||||
handleFocus = () => {
|
||||
this.props.setActiveStyle();
|
||||
}
|
||||
|
||||
handleBlur = (e) => {
|
||||
const listValue = e.target.value.split(',').map(el => el.trim()).filter(el => el);
|
||||
this.setState({ value: valueToString(listValue) });
|
||||
this.props.setInactiveStyle();
|
||||
}
|
||||
|
||||
handleAdd = (e) => {
|
||||
e.preventDefault();
|
||||
const { value, onChange } = this.props;
|
||||
const parsedValue = (this.valueType === valueTypes.SINGLE) ? null : Map();
|
||||
this.setState({ itemsCollapsed: this.state.itemsCollapsed.push(false) });
|
||||
onChange((value || List()).push(parsedValue));
|
||||
}
|
||||
|
||||
/**
|
||||
* In case the `onChangeObject` function is frozen by a child widget implementation,
|
||||
* e.g. when debounced, always get the latest object value instead of using
|
||||
* `this.props.value` directly.
|
||||
*/
|
||||
getObjectValue = idx => this.props.value.get(idx) || Map();
|
||||
|
||||
handleChangeFor(index) {
|
||||
return (fieldName, newValue, newMetadata) => {
|
||||
const { value, metadata, onChange, field } = this.props;
|
||||
const collectionName = field.get('name');
|
||||
const newObjectValue = this.getObjectValue(index).set(fieldName, newValue);
|
||||
const parsedValue = (this.valueType === valueTypes.SINGLE) ? newObjectValue.first() : newObjectValue;
|
||||
const parsedMetadata = {
|
||||
[collectionName]: Object.assign(metadata ? metadata.toJS() : {}, newMetadata ? newMetadata[collectionName] : {}),
|
||||
};
|
||||
onChange(value.set(index, parsedValue), parsedMetadata);
|
||||
};
|
||||
}
|
||||
|
||||
handleRemove = (index, event) => {
|
||||
event.preventDefault();
|
||||
const { itemsCollapsed } = this.state;
|
||||
const { value, metadata, onChange, field } = this.props;
|
||||
const collectionName = field.get('name');
|
||||
const parsedMetadata = metadata && { [collectionName]: metadata.removeIn(value.get(index).valueSeq()) };
|
||||
|
||||
this.setState({ itemsCollapsed: itemsCollapsed.delete(index) });
|
||||
|
||||
onChange(value.remove(index), parsedMetadata);
|
||||
};
|
||||
|
||||
handleItemCollapseToggle = (index, event) => {
|
||||
event.preventDefault();
|
||||
const { itemsCollapsed } = this.state;
|
||||
const collapsed = itemsCollapsed.get(index);
|
||||
this.setState({ itemsCollapsed: itemsCollapsed.set(index, !collapsed) });
|
||||
};
|
||||
|
||||
handleCollapseAllToggle = (e) => {
|
||||
e.preventDefault();
|
||||
const { value } = this.props;
|
||||
const { itemsCollapsed } = this.state;
|
||||
const allItemsCollapsed = itemsCollapsed.every(val => val === true);
|
||||
this.setState({ itemsCollapsed: List(Array(value.size).fill(!allItemsCollapsed)) });
|
||||
};
|
||||
|
||||
objectLabel(item) {
|
||||
const { field } = this.props;
|
||||
const multiFields = field.get('fields');
|
||||
const singleField = field.get('field');
|
||||
const labelField = (multiFields && multiFields.first()) || singleField;
|
||||
const value = multiFields ? item.get(multiFields.first().get('name')) : singleField.get('label');
|
||||
return (value || `No ${ labelField.get('name') }`).toString();
|
||||
}
|
||||
|
||||
onSortEnd = ({ oldIndex, newIndex }) => {
|
||||
const { value, onChange } = this.props;
|
||||
const { itemsCollapsed } = this.state;
|
||||
|
||||
// Update value
|
||||
const item = value.get(oldIndex);
|
||||
const newValue = value.delete(oldIndex).insert(newIndex, item);
|
||||
this.props.onChange(newValue);
|
||||
|
||||
// Update collapsing
|
||||
const collapsed = itemsCollapsed.get(oldIndex);
|
||||
const updatedItemsCollapsed = itemsCollapsed.delete(oldIndex).insert(newIndex, collapsed);
|
||||
this.setState({ itemsCollapsed: updatedItemsCollapsed });
|
||||
};
|
||||
|
||||
renderItem = (item, index) => {
|
||||
const {
|
||||
field,
|
||||
getAsset,
|
||||
mediaPaths,
|
||||
onOpenMediaLibrary,
|
||||
onAddAsset,
|
||||
onRemoveInsertedMedia,
|
||||
classNameWrapper,
|
||||
} = this.props;
|
||||
const { itemsCollapsed } = this.state;
|
||||
const collapsed = itemsCollapsed.get(index);
|
||||
const classNames = ['nc-listControl-item', collapsed ? 'nc-listControl-collapsed' : ''];
|
||||
|
||||
return (<SortableListItem className={classNames.join(' ')} index={index} key={`item-${ index }`}>
|
||||
<ListItemTopBar
|
||||
className="nc-listControl-itemTopBar"
|
||||
collapsed={collapsed}
|
||||
onCollapseToggle={partial(this.handleItemCollapseToggle, index)}
|
||||
onRemove={partial(this.handleRemove, index)}
|
||||
dragHandleHOC={SortableHandle}
|
||||
/>
|
||||
<div className="nc-listControl-objectLabel">{this.objectLabel(item)}</div>
|
||||
<ObjectControl
|
||||
value={item}
|
||||
field={field}
|
||||
onChangeObject={this.handleChangeFor(index)}
|
||||
getAsset={getAsset}
|
||||
onOpenMediaLibrary={onOpenMediaLibrary}
|
||||
mediaPaths={mediaPaths}
|
||||
onAddAsset={onAddAsset}
|
||||
onRemoveInsertedMedia={onRemoveInsertedMedia}
|
||||
classNameWrapper={`${ classNameWrapper } nc-listControl-objectControl`}
|
||||
forList
|
||||
/>
|
||||
</SortableListItem>);
|
||||
};
|
||||
|
||||
renderListControl() {
|
||||
const { value, forID, field, classNameWrapper } = this.props;
|
||||
const { itemsCollapsed } = this.state;
|
||||
const items = value || List();
|
||||
const label = field.get('label');
|
||||
const labelSingular = field.get('label_singular') || field.get('label');
|
||||
|
||||
return (
|
||||
<div id={forID} className={c(classNameWrapper, 'nc-listControl')}>
|
||||
<TopBar
|
||||
allowAdd={field.get('allow_add', true)}
|
||||
onAdd={this.handleAdd}
|
||||
listLabel={items.size === 1 ? labelSingular.toLowerCase() : label.toLowerCase()}
|
||||
onCollapseAllToggle={this.handleCollapseAllToggle}
|
||||
allItemsCollapsed={itemsCollapsed.every(val => val === true)}
|
||||
itemsCount={items.size}
|
||||
/>
|
||||
<SortableList
|
||||
items={items}
|
||||
renderItem={this.renderItem}
|
||||
onSortEnd={this.onSortEnd}
|
||||
useDragHandle
|
||||
lockAxis="y"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { field, forID, classNameWrapper } = this.props;
|
||||
const { value } = this.state;
|
||||
|
||||
if (field.get('field') || field.get('fields')) {
|
||||
return this.renderListControl();
|
||||
}
|
||||
|
||||
return (<input
|
||||
type="text"
|
||||
id={forID}
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleBlur}
|
||||
className={classNameWrapper}
|
||||
/>);
|
||||
}
|
||||
};
|
@ -1,11 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ObjectPreview from 'EditorWidgets/Object/ObjectPreview';
|
||||
|
||||
const ListPreview = ObjectPreview;
|
||||
|
||||
ListPreview.propTypes = {
|
||||
field: PropTypes.node,
|
||||
};
|
||||
|
||||
export default ListPreview;
|
@ -1,12 +0,0 @@
|
||||
.nc-objectControl-root {
|
||||
padding: 0 14px 14px;
|
||||
}
|
||||
|
||||
.nc-objectControl-topBar {
|
||||
align-items: center;
|
||||
background-color: #dfdfe3;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 0 -14px;
|
||||
padding: 13px;
|
||||
}
|
@ -1,119 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { Map } from 'immutable';
|
||||
import { partial } from 'lodash';
|
||||
import c from 'classnames';
|
||||
import { Icon } from 'netlify-cms-ui-default';
|
||||
import { resolveWidget } from 'Lib/registry';
|
||||
|
||||
const TopBar = ({ collapsed, onCollapseToggle }) => (
|
||||
<div className="nc-objectControl-topBar">
|
||||
<div className="nc-objectControl-objectCollapseToggle">
|
||||
<button className="nc-listControl-listCollapseToggleButton" onClick={onCollapseToggle}>
|
||||
<Icon type="chevron" direction={collapsed ? 'right' : 'down'} size="small" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default class ObjectControl extends Component {
|
||||
static propTypes = {
|
||||
onChangeObject: PropTypes.func.isRequired,
|
||||
onOpenMediaLibrary: PropTypes.func.isRequired,
|
||||
mediaPaths: ImmutablePropTypes.map.isRequired,
|
||||
onAddAsset: PropTypes.func.isRequired,
|
||||
onRemoveInsertedMedia: PropTypes.func.isRequired,
|
||||
getAsset: PropTypes.func.isRequired,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.node,
|
||||
PropTypes.object,
|
||||
PropTypes.bool,
|
||||
]),
|
||||
field: PropTypes.object,
|
||||
forID: PropTypes.string,
|
||||
classNameWrapper: PropTypes.string.isRequired,
|
||||
forList: PropTypes.bool,
|
||||
editorControl: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
value: Map(),
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
collapsed: false,
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* Always update so that each nested widget has the option to update. This is
|
||||
* required because ControlHOC provides a default `shouldComponentUpdate`
|
||||
* which only updates if the value changes, but every widget must be allowed
|
||||
* to override this.
|
||||
*/
|
||||
shouldComponentUpdate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
controlFor(field, key) {
|
||||
const {
|
||||
onAddAsset,
|
||||
onOpenMediaLibrary,
|
||||
mediaPaths,
|
||||
onRemoveInsertedMedia,
|
||||
getAsset,
|
||||
value,
|
||||
onChangeObject,
|
||||
editorControl: EditorControl,
|
||||
} = this.props;
|
||||
|
||||
if (field.get('widget') === 'hidden') {
|
||||
return null;
|
||||
}
|
||||
const widgetName = field.get('widget') || 'string';
|
||||
const widget = resolveWidget(widgetName);
|
||||
const fieldName = field.get('name');
|
||||
const fieldValue = value && Map.isMap(value) ? value.get(fieldName) : value;
|
||||
|
||||
return (
|
||||
<EditorControl
|
||||
key={key}
|
||||
field={field}
|
||||
value={fieldValue}
|
||||
mediaPaths={mediaPaths}
|
||||
getAsset={getAsset}
|
||||
onChange={onChangeObject}
|
||||
onOpenMediaLibrary={onOpenMediaLibrary}
|
||||
onAddAsset={onAddAsset}
|
||||
onRemoveInsertedMedia={onRemoveInsertedMedia}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
handleCollapseToggle = () => {
|
||||
this.setState({ collapsed: !this.state.collapsed });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { field, forID, classNameWrapper, forList } = this.props;
|
||||
const { collapsed } = this.state;
|
||||
const multiFields = field.get('fields');
|
||||
const singleField = field.get('field');
|
||||
|
||||
if (multiFields) {
|
||||
return (
|
||||
<div id={forID} className={c(classNameWrapper, 'nc-objectControl-root')}>
|
||||
{ forList ? null : <TopBar collapsed={collapsed} onCollapseToggle={this.handleCollapseToggle} /> }
|
||||
{ collapsed ? null : multiFields.map((f, idx) => this.controlFor(f, idx)) }
|
||||
</div>
|
||||
);
|
||||
} else if (singleField) {
|
||||
return this.controlFor(singleField);
|
||||
}
|
||||
|
||||
return <h3>No field(s) defined for this widget</h3>;
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
const ObjectPreview = ({ field }) => (
|
||||
<div className="nc-widgetPreview">{(field && field.get('fields')) || null}</div>
|
||||
);
|
||||
|
||||
ObjectPreview.propTypes = {
|
||||
field: PropTypes.node,
|
||||
};
|
||||
|
||||
export default ObjectPreview;
|
@ -9,18 +9,12 @@ import SelectControl from './Select/SelectControl';
|
||||
import SelectPreview from './Select/SelectPreview';
|
||||
import MarkdownControl from './Markdown/MarkdownControl';
|
||||
import MarkdownPreview from './Markdown/MarkdownPreview';
|
||||
import ListControl from './List/ListControl';
|
||||
import ListPreview from './List/ListPreview';
|
||||
import ObjectControl from './Object/ObjectControl';
|
||||
import ObjectPreview from './Object/ObjectPreview';
|
||||
import RelationControl from './Relation/RelationControl';
|
||||
import RelationPreview from './Relation/RelationPreview';
|
||||
|
||||
registerWidget('text', TextControl, TextPreview);
|
||||
registerWidget('number', NumberControl, NumberPreview);
|
||||
registerWidget('list', ListControl, ListPreview);
|
||||
registerWidget('markdown', MarkdownControl, MarkdownPreview);
|
||||
registerWidget('select', SelectControl, SelectPreview);
|
||||
registerWidget('object', ObjectControl, ObjectPreview);
|
||||
registerWidget('relation', RelationControl, RelationPreview);
|
||||
registerWidget('unknown', UnknownControl, UnknownPreview);
|
||||
|
@ -1,34 +0,0 @@
|
||||
.nc-imageControl-content {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.nc-imageControl-imageWrapper {
|
||||
width: 155px;
|
||||
height: 100px;
|
||||
margin-right: 20px;
|
||||
|
||||
& img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: var(--borderRadius);
|
||||
}
|
||||
}
|
||||
|
||||
.nc-imageControl-filename {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.nc-imageControl-chooseButton,
|
||||
.nc-imageControl-changeButton {
|
||||
@apply(--textBadge);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nc-imageControl-removeButton {
|
||||
@apply(--textBadgeDanger);
|
||||
display: block;
|
||||
margin-top: 12px;
|
||||
}
|
@ -1,120 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from "react-immutable-proptypes";
|
||||
import uuid from 'uuid/v4';
|
||||
import { truncateMiddle } from 'Lib/textHelper';
|
||||
|
||||
const MAX_DISPLAY_LENGTH = 50;
|
||||
|
||||
export default function withMediaControl(forImage) {
|
||||
return class extends React.Component {
|
||||
static propTypes = {
|
||||
field: PropTypes.object.isRequired,
|
||||
getAsset: PropTypes.func.isRequired,
|
||||
mediaPaths: ImmutablePropTypes.map.isRequired,
|
||||
onAddAsset: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onRemoveInsertedMedia: PropTypes.func.isRequired,
|
||||
onOpenMediaLibrary: PropTypes.func.isRequired,
|
||||
classNameWrapper: PropTypes.string.isRequired,
|
||||
value: PropTypes.node,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
value: '',
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.controlID = uuid();
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
/**
|
||||
* Always update if the value changes.
|
||||
*/
|
||||
if (this.props.value !== nextProps.value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is a media path for this control in the state object, and that
|
||||
* path is different than the value in `nextProps`, update.
|
||||
*/
|
||||
const mediaPath = nextProps.mediaPaths.get(this.controlID);
|
||||
if (mediaPath && (nextProps.value !== mediaPath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { mediaPaths, value, onRemoveInsertedMedia, onChange } = nextProps;
|
||||
const mediaPath = mediaPaths.get(this.controlID);
|
||||
if (mediaPath && mediaPath !== value) {
|
||||
onChange(mediaPath);
|
||||
} else if (mediaPath && mediaPath === value) {
|
||||
onRemoveInsertedMedia(this.controlID);
|
||||
}
|
||||
}
|
||||
|
||||
handleChange = e => {
|
||||
const { field, onOpenMediaLibrary} = this.props;
|
||||
e.preventDefault();
|
||||
return onOpenMediaLibrary({
|
||||
controlID: this.controlID,
|
||||
forImage,
|
||||
privateUpload: field.get('private'),
|
||||
});
|
||||
};
|
||||
|
||||
handleRemove = e => {
|
||||
e.preventDefault();
|
||||
return this.props.onChange('');
|
||||
};
|
||||
|
||||
renderFileName = () => {
|
||||
const { value, classNameWrapper } = this.props;
|
||||
return value ? truncateMiddle(value, MAX_DISPLAY_LENGTH) : null;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { value, getAsset, onRemoveAsset, classNameWrapper } = this.props;
|
||||
const fileName = this.renderFileName();
|
||||
const subject = forImage ? 'image' : 'file';
|
||||
const article = forImage ? 'an' : 'a';
|
||||
|
||||
return (
|
||||
<div className={`${classNameWrapper} nc-imageControl-imageUpload`}>
|
||||
<span className="nc-imageControl-message">
|
||||
{
|
||||
fileName
|
||||
? <div className="nc-imageControl-content">
|
||||
{
|
||||
forImage
|
||||
? <div className="nc-imageControl-imageWrapper">
|
||||
<img src={getAsset(value)}/>
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
<div>
|
||||
<span className="nc-imageControl-filename">{fileName}</span>
|
||||
<button className="nc-imageControl-changeButton" onClick={this.handleChange}>
|
||||
Choose different {subject}
|
||||
</button>
|
||||
<button className="nc-imageControl-removeButton" onClick={this.handleRemove}>
|
||||
Remove {subject}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
: <button className="nc-imageControl-chooseButton" onClick={this.handleChange}>
|
||||
Choose {article} {subject}
|
||||
</button>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
Reference in New Issue
Block a user