migrate object and list widgets
This commit is contained in:
parent
2efd09ba94
commit
3f47fe6dbf
@ -8,13 +8,13 @@ import { DateControl, DatePreview } from 'netlify-cms-widget-date';
|
|||||||
import { DateTimeControl, DateTimePreview } from 'netlify-cms-widget-datetime';
|
import { DateTimeControl, DateTimePreview } from 'netlify-cms-widget-datetime';
|
||||||
import { FileControl, FilePreview } from 'netlify-cms-widget-file';
|
import { FileControl, FilePreview } from 'netlify-cms-widget-file';
|
||||||
import { ImageControl, ImagePreview } from 'netlify-cms-widget-image';
|
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 { StringControl, StringPreview } from 'netlify-cms-widget-string';
|
||||||
// import { NumberControl, NumberPreview } from 'netlify-cms-widget-number';
|
// import { NumberControl, NumberPreview } from 'netlify-cms-widget-number';
|
||||||
// import { TextControl, TextPreview } from 'netlify-cms-widget-text';
|
// import { TextControl, TextPreview } from 'netlify-cms-widget-text';
|
||||||
// import { SelectControl, SelectPreview } from 'netlify-cms-widget-select';
|
// import { SelectControl, SelectPreview } from 'netlify-cms-widget-select';
|
||||||
// import { MarkdownControl, MarkdownPreview } from 'netlify-cms-widget-markdown';
|
// 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 { RelationControl, RelationPreview } from 'netlify-cms-widget-relation';
|
||||||
import image from 'netlify-cms-editor-component-image';
|
import image from 'netlify-cms-editor-component-image';
|
||||||
|
|
||||||
@ -27,12 +27,12 @@ registerWidget('date', DateControl, DatePreview);
|
|||||||
registerWidget('datetime', DateTimeControl, DateTimePreview);
|
registerWidget('datetime', DateTimeControl, DateTimePreview);
|
||||||
registerWidget('file', FileControl, FilePreview);
|
registerWidget('file', FileControl, FilePreview);
|
||||||
registerWidget('image', ImageControl, ImagePreview);
|
registerWidget('image', ImageControl, ImagePreview);
|
||||||
|
registerWidget('list', ListControl, ListPreview);
|
||||||
|
registerWidget('object', ObjectControl, ObjectPreview);
|
||||||
registerWidget('string', StringControl, StringPreview);
|
registerWidget('string', StringControl, StringPreview);
|
||||||
// registerWidget('text', TextControl, TextPreview);
|
// registerWidget('text', TextControl, TextPreview);
|
||||||
// registerWidget('number', NumberControl, NumberPreview);
|
// registerWidget('number', NumberControl, NumberPreview);
|
||||||
// registerWidget('list', ListControl, ListPreview);
|
|
||||||
// registerWidget('markdown', MarkdownControl, MarkdownPreview);
|
// registerWidget('markdown', MarkdownControl, MarkdownPreview);
|
||||||
// registerWidget('select', SelectControl, SelectPreview);
|
// registerWidget('select', SelectControl, SelectPreview);
|
||||||
// registerWidget('object', ObjectControl, ObjectPreview);
|
|
||||||
// registerWidget('relation', RelationControl, RelationPreview);
|
// registerWidget('relation', RelationControl, RelationPreview);
|
||||||
registerEditorComponent(image);
|
registerEditorComponent(image);
|
||||||
|
@ -172,6 +172,7 @@ export default class EditorControl extends React.Component {
|
|||||||
hasActiveStyle={this.state.styleActive}
|
hasActiveStyle={this.state.styleActive}
|
||||||
setActiveStyle={() => this.setState({ styleActive: true })}
|
setActiveStyle={() => this.setState({ styleActive: true })}
|
||||||
setInactiveStyle={() => this.setState({ styleActive: false })}
|
setInactiveStyle={() => this.setState({ styleActive: false })}
|
||||||
|
resolveWidget={resolveWidget}
|
||||||
ref={processControlRef && partial(processControlRef, fieldName)}
|
ref={processControlRef && partial(processControlRef, fieldName)}
|
||||||
editorControl={EditorControl}
|
editorControl={EditorControl}
|
||||||
/>
|
/>
|
||||||
|
@ -39,6 +39,7 @@ export default class Widget extends Component {
|
|||||||
onAddAsset: PropTypes.func.isRequired,
|
onAddAsset: PropTypes.func.isRequired,
|
||||||
onRemoveInsertedMedia: PropTypes.func.isRequired,
|
onRemoveInsertedMedia: PropTypes.func.isRequired,
|
||||||
getAsset: PropTypes.func.isRequired,
|
getAsset: PropTypes.func.isRequired,
|
||||||
|
resolveWidget: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps) {
|
shouldComponentUpdate(nextProps) {
|
||||||
@ -189,7 +190,8 @@ export default class Widget extends Component {
|
|||||||
setInactiveStyle,
|
setInactiveStyle,
|
||||||
hasActiveStyle,
|
hasActiveStyle,
|
||||||
editorControl,
|
editorControl,
|
||||||
uniqueFieldId
|
uniqueFieldId,
|
||||||
|
resolveWidget,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
return React.createElement(controlComponent, {
|
return React.createElement(controlComponent, {
|
||||||
field,
|
field,
|
||||||
@ -213,6 +215,7 @@ export default class Widget extends Component {
|
|||||||
setInactiveStyle,
|
setInactiveStyle,
|
||||||
hasActiveStyle,
|
hasActiveStyle,
|
||||||
editorControl,
|
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,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,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 SelectPreview from './Select/SelectPreview';
|
||||||
import MarkdownControl from './Markdown/MarkdownControl';
|
import MarkdownControl from './Markdown/MarkdownControl';
|
||||||
import MarkdownPreview from './Markdown/MarkdownPreview';
|
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 RelationControl from './Relation/RelationControl';
|
||||||
import RelationPreview from './Relation/RelationPreview';
|
import RelationPreview from './Relation/RelationPreview';
|
||||||
|
|
||||||
registerWidget('text', TextControl, TextPreview);
|
registerWidget('text', TextControl, TextPreview);
|
||||||
registerWidget('number', NumberControl, NumberPreview);
|
registerWidget('number', NumberControl, NumberPreview);
|
||||||
registerWidget('list', ListControl, ListPreview);
|
|
||||||
registerWidget('markdown', MarkdownControl, MarkdownPreview);
|
registerWidget('markdown', MarkdownControl, MarkdownPreview);
|
||||||
registerWidget('select', SelectControl, SelectPreview);
|
registerWidget('select', SelectControl, SelectPreview);
|
||||||
registerWidget('object', ObjectControl, ObjectPreview);
|
|
||||||
registerWidget('relation', RelationControl, RelationPreview);
|
registerWidget('relation', RelationControl, RelationPreview);
|
||||||
registerWidget('unknown', UnknownControl, UnknownPreview);
|
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,9 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'react-emotion';
|
import styled from 'react-emotion';
|
||||||
import Icon from './Icon';
|
import Icon from './Icon';
|
||||||
import { colors, lengths } from './styles';
|
import { colors, lengths, buttons } from './styles';
|
||||||
|
|
||||||
|
const TopBar = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 26px;
|
||||||
|
border-radius: ${lengths.borderRadius} ${lengths.borderRadius} 0 0;
|
||||||
|
position: relative;
|
||||||
|
`
|
||||||
|
|
||||||
const TopBarButton = styled.button`
|
const TopBarButton = styled.button`
|
||||||
|
${buttons.button};
|
||||||
color: ${colors.controlLabel};
|
color: ${colors.controlLabel};
|
||||||
background: transparent;
|
background: transparent;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
@ -24,8 +33,8 @@ const DragIcon = styled(TopBarButtonSpan)`
|
|||||||
cursor: move;
|
cursor: move;
|
||||||
`
|
`
|
||||||
|
|
||||||
const ListItemTopBar = ({ collapsed, onCollapseToggle, onRemove, dragHandleHOC }) => (
|
const ListItemTopBar = ({ className, collapsed, onCollapseToggle, onRemove, dragHandleHOC }) => (
|
||||||
<div>
|
<TopBar className={className}>
|
||||||
{
|
{
|
||||||
onCollapseToggle
|
onCollapseToggle
|
||||||
? <TopBarButton onClick={onCollapseToggle}>
|
? <TopBarButton onClick={onCollapseToggle}>
|
||||||
@ -47,7 +56,7 @@ const ListItemTopBar = ({ collapsed, onCollapseToggle, onRemove, dragHandleHOC }
|
|||||||
</TopBarButton>
|
</TopBarButton>
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
</div>
|
</TopBar>
|
||||||
);
|
);
|
||||||
|
|
||||||
const StyledListItemTopBar = styled(ListItemTopBar)`
|
const StyledListItemTopBar = styled(ListItemTopBar)`
|
||||||
|
67
packages/netlify-cms-ui-default/src/ObjectWidgetTopBar.js
Normal file
67
packages/netlify-cms-ui-default/src/ObjectWidgetTopBar.js
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styled, { css }from 'react-emotion';
|
||||||
|
import Icon from './Icon';
|
||||||
|
import { colors, buttons } from './styles';
|
||||||
|
|
||||||
|
const TopBarContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
background-color: ${colors.textFieldBorder};
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin: 0 -14px;
|
||||||
|
padding: 13px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ExpandButtonContainer = styled.div`
|
||||||
|
${props => props.hasHeading && css`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1;
|
||||||
|
`}
|
||||||
|
`
|
||||||
|
|
||||||
|
const ExpandButton = styled.button`
|
||||||
|
${buttons.button};
|
||||||
|
padding: 4px;
|
||||||
|
background-color: transparent;
|
||||||
|
color: inherit;
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const AddButton = styled.button`
|
||||||
|
${buttons.button};
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
${Icon} {
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const ObjectWidgetTopBar = ({ allowAdd, onAdd, onCollapseToggle, collapsed, heading = null, label }) => (
|
||||||
|
<TopBarContainer>
|
||||||
|
<ExpandButtonContainer hasHeading={!!heading}>
|
||||||
|
<ExpandButton onClick={onCollapseToggle}>
|
||||||
|
<Icon type="chevron" direction={collapsed ? 'right' : 'down'} size="small" />
|
||||||
|
</ExpandButton>
|
||||||
|
{heading}
|
||||||
|
</ExpandButtonContainer>
|
||||||
|
{!allowAdd ? null :
|
||||||
|
<AddButton onClick={onAdd}>
|
||||||
|
Add {label} <Icon type="add" size="xsmall" />
|
||||||
|
</AddButton>
|
||||||
|
}
|
||||||
|
</TopBarContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ObjectWidgetTopBar;
|
@ -5,6 +5,7 @@ export Loader from './Loader';
|
|||||||
export Toggle, { ToggleContainer, ToggleBackground, ToggleHandle } from './Toggle';
|
export Toggle, { ToggleContainer, ToggleBackground, ToggleHandle } from './Toggle';
|
||||||
export AuthenticationPage from './AuthenticationPage';
|
export AuthenticationPage from './AuthenticationPage';
|
||||||
export WidgetPreviewContainer from './WidgetPreviewContainer';
|
export WidgetPreviewContainer from './WidgetPreviewContainer';
|
||||||
|
export ObjectWidgetTopBar from './ObjectWidgetTopBar';
|
||||||
export {
|
export {
|
||||||
fonts,
|
fonts,
|
||||||
colorsRaw,
|
colorsRaw,
|
||||||
|
@ -253,6 +253,9 @@ const components = {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
`,
|
`,
|
||||||
|
objectWidgetTopBarContainer: css`
|
||||||
|
padding: 0 14px 14px;
|
||||||
|
`,
|
||||||
}
|
}
|
||||||
|
|
||||||
injectGlobal`
|
injectGlobal`
|
||||||
|
39
packages/netlify-cms-widget-list/package.json
Normal file
39
packages/netlify-cms-widget-list/package.json
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "netlify-cms-widget-list",
|
||||||
|
"description": "Widget for editing lists in Netlify CMS.",
|
||||||
|
"version": "2.0.0-alpha.0",
|
||||||
|
"main": "dist/netlify-cms-widget-list.js",
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"netlify",
|
||||||
|
"netlify-cms",
|
||||||
|
"widget",
|
||||||
|
"list",
|
||||||
|
"object"
|
||||||
|
],
|
||||||
|
"sideEffects": false,
|
||||||
|
"scripts": {
|
||||||
|
"watch": "webpack -w",
|
||||||
|
"build": "webpack"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"netlify-cms-widget-object": "^2.0.0-alpha.0",
|
||||||
|
"react-sortable-hoc": "^0.6.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"webpack": "^4.16.1",
|
||||||
|
"webpack-cli": "^3.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"immutable": "^3.7.6",
|
||||||
|
"lodash": "^4.17.10",
|
||||||
|
"netlify-cms-ui-default": "^2.0.0-alpha.0",
|
||||||
|
"prop-types": "^15.5.10",
|
||||||
|
"react": "^16.4.1",
|
||||||
|
"react-emotion": "^9.2.6",
|
||||||
|
"react-immutable-proptypes": "^2.1.0"
|
||||||
|
},
|
||||||
|
"localExternals": [
|
||||||
|
"netlify-cms-widget-object"
|
||||||
|
]
|
||||||
|
}
|
@ -1,47 +1,55 @@
|
|||||||
import React, { Component } from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import styled, { cx, css } from 'react-emotion';
|
||||||
import { List, Map } from 'immutable';
|
import { List, Map } from 'immutable';
|
||||||
import { partial } from 'lodash';
|
import { partial } from 'lodash';
|
||||||
import c from 'classnames';
|
|
||||||
import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc';
|
import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc';
|
||||||
import { Icon, ListItemTopBar } from 'netlify-cms-ui-default';
|
import { ObjectControl } from 'netlify-cms-widget-object';
|
||||||
import ObjectControl from 'EditorWidgets/Object/ObjectControl';
|
import {
|
||||||
|
Icon,
|
||||||
function ListItem(props) {
|
ListItemTopBar,
|
||||||
return <div {...props} className={`list-item ${ props.className || '' }`}>{props.children}</div>;
|
ObjectWidgetTopBar,
|
||||||
}
|
colors,
|
||||||
ListItem.propTypes = {
|
lengths,
|
||||||
className: PropTypes.string,
|
components,
|
||||||
children: PropTypes.node,
|
} from 'netlify-cms-ui-default';
|
||||||
};
|
|
||||||
ListItem.displayName = 'list-item';
|
|
||||||
|
|
||||||
function valueToString(value) {
|
function valueToString(value) {
|
||||||
return value ? value.join(',').replace(/,([^\s]|$)/g, ', $1') : '';
|
return value ? value.join(',').replace(/,([^\s]|$)/g, ', $1') : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ListItem = styled.div();
|
||||||
|
|
||||||
const SortableListItem = SortableElement(ListItem);
|
const SortableListItem = SortableElement(ListItem);
|
||||||
|
|
||||||
const TopBar = ({ allowAdd, onAdd, listLabel, onCollapseAllToggle, allItemsCollapsed, itemsCount }) => (
|
const StyledListItemTopBar = styled(ListItemTopBar)`
|
||||||
<div className="nc-listControl-topBar">
|
background-color: ${colors.textFieldBorder};
|
||||||
<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>
|
|
||||||
|
|
||||||
{
|
const NestedObjectLabel = styled.div`
|
||||||
allowAdd ?
|
display: ${props => props.collapsed ? 'block' : 'none'};
|
||||||
<button className="nc-listControl-addButton" onClick={onAdd}>
|
border-top: 0;
|
||||||
Add {listLabel} <Icon type="add" size="xsmall" />
|
background-color: ${colors.textFieldBorder};
|
||||||
</button>
|
padding: 13px;
|
||||||
:
|
border-radius: 0 0 ${lengths.borderRadius} ${lengths.borderRadius};
|
||||||
null
|
`
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
collapsedObjectControl: css`
|
||||||
|
display: none;
|
||||||
|
`,
|
||||||
|
listControlItem: css`
|
||||||
|
margin-top: 18px;
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
margin-top: 26px;
|
||||||
}
|
}
|
||||||
</div>
|
`,
|
||||||
);
|
listControlItemCollapsed: css`
|
||||||
|
padding-bottom: 0;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
const SortableList = SortableContainer(({ items, renderItem }) => {
|
const SortableList = SortableContainer(({ items, renderItem }) => {
|
||||||
return <div>{items.map(renderItem)}</div>;
|
return <div>{items.map(renderItem)}</div>;
|
||||||
@ -52,7 +60,7 @@ const valueTypes = {
|
|||||||
MULTIPLE: 'MULTIPLE',
|
MULTIPLE: 'MULTIPLE',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class ListControl extends Component {
|
export default class ListControl extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
onChangeObject: PropTypes.func.isRequired,
|
onChangeObject: PropTypes.func.isRequired,
|
||||||
@ -229,33 +237,44 @@ export default class ListControl extends Component {
|
|||||||
onAddAsset,
|
onAddAsset,
|
||||||
onRemoveInsertedMedia,
|
onRemoveInsertedMedia,
|
||||||
classNameWrapper,
|
classNameWrapper,
|
||||||
|
editorControl,
|
||||||
|
resolveWidget,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { itemsCollapsed } = this.state;
|
const { itemsCollapsed } = this.state;
|
||||||
const collapsed = itemsCollapsed.get(index);
|
const collapsed = itemsCollapsed.get(index);
|
||||||
const classNames = ['nc-listControl-item', collapsed ? 'nc-listControl-collapsed' : ''];
|
|
||||||
|
|
||||||
return (<SortableListItem className={classNames.join(' ')} index={index} key={`item-${ index }`}>
|
return (
|
||||||
<ListItemTopBar
|
<SortableListItem
|
||||||
className="nc-listControl-itemTopBar"
|
className={cx(styles.listControlItem, { [styles.listControlItemCollapsed]: collapsed })}
|
||||||
collapsed={collapsed}
|
index={index}
|
||||||
onCollapseToggle={partial(this.handleItemCollapseToggle, index)}
|
key={`item-${ index }`}
|
||||||
onRemove={partial(this.handleRemove, index)}
|
>
|
||||||
dragHandleHOC={SortableHandle}
|
<StyledListItemTopBar
|
||||||
/>
|
collapsed={collapsed}
|
||||||
<div className="nc-listControl-objectLabel">{this.objectLabel(item)}</div>
|
onCollapseToggle={partial(this.handleItemCollapseToggle, index)}
|
||||||
<ObjectControl
|
onRemove={partial(this.handleRemove, index)}
|
||||||
value={item}
|
dragHandleHOC={SortableHandle}
|
||||||
field={field}
|
/>
|
||||||
onChangeObject={this.handleChangeFor(index)}
|
<NestedObjectLabel collapsed={collapsed}>{this.objectLabel(item)}</NestedObjectLabel>
|
||||||
getAsset={getAsset}
|
<ObjectControl
|
||||||
onOpenMediaLibrary={onOpenMediaLibrary}
|
value={item}
|
||||||
mediaPaths={mediaPaths}
|
field={field}
|
||||||
onAddAsset={onAddAsset}
|
onChangeObject={this.handleChangeFor(index)}
|
||||||
onRemoveInsertedMedia={onRemoveInsertedMedia}
|
getAsset={getAsset}
|
||||||
classNameWrapper={`${ classNameWrapper } nc-listControl-objectControl`}
|
onOpenMediaLibrary={onOpenMediaLibrary}
|
||||||
forList
|
mediaPaths={mediaPaths}
|
||||||
/>
|
onAddAsset={onAddAsset}
|
||||||
</SortableListItem>);
|
onRemoveInsertedMedia={onRemoveInsertedMedia}
|
||||||
|
classNameWrapper={cx(
|
||||||
|
classNameWrapper,
|
||||||
|
{ [styles.collapsedObjectControl]: collapsed },
|
||||||
|
)}
|
||||||
|
editorControl={editorControl}
|
||||||
|
resolveWidget={resolveWidget}
|
||||||
|
forList
|
||||||
|
/>
|
||||||
|
</SortableListItem>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
renderListControl() {
|
renderListControl() {
|
||||||
@ -264,16 +283,17 @@ export default class ListControl extends Component {
|
|||||||
const items = value || List();
|
const items = value || List();
|
||||||
const label = field.get('label');
|
const label = field.get('label');
|
||||||
const labelSingular = field.get('label_singular') || field.get('label');
|
const labelSingular = field.get('label_singular') || field.get('label');
|
||||||
|
const listLabel = items.size === 1 ? labelSingular.toLowerCase() : label.toLowerCase();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id={forID} className={c(classNameWrapper, 'nc-listControl')}>
|
<div id={forID} className={cx(classNameWrapper, components.objectWidgetTopBarContainer)}>
|
||||||
<TopBar
|
<ObjectWidgetTopBar
|
||||||
allowAdd={field.get('allow_add', true)}
|
allowAdd={field.get('allow_add', true)}
|
||||||
onAdd={this.handleAdd}
|
onAdd={this.handleAdd}
|
||||||
listLabel={items.size === 1 ? labelSingular.toLowerCase() : label.toLowerCase()}
|
heading={`${items.size} ${listLabel}`}
|
||||||
onCollapseAllToggle={this.handleCollapseAllToggle}
|
label={listLabel}
|
||||||
allItemsCollapsed={itemsCollapsed.every(val => val === true)}
|
onCollapseToggle={this.handleCollapseAllToggle}
|
||||||
itemsCount={items.size}
|
collapsed={itemsCollapsed.every(val => val === true)}
|
||||||
/>
|
/>
|
||||||
<SortableList
|
<SortableList
|
||||||
items={items}
|
items={items}
|
2
packages/netlify-cms-widget-list/src/index.js
Normal file
2
packages/netlify-cms-widget-list/src/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export ListControl from './ListControl';
|
||||||
|
export { ObjectPreview as ListPreview } from 'netlify-cms-widget-object';
|
3
packages/netlify-cms-widget-list/webpack.config.js
Normal file
3
packages/netlify-cms-widget-list/webpack.config.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
const { getConfig } = require('../../scripts/webpack.js');
|
||||||
|
|
||||||
|
module.exports = getConfig();
|
33
packages/netlify-cms-widget-object/package.json
Normal file
33
packages/netlify-cms-widget-object/package.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "netlify-cms-widget-object",
|
||||||
|
"description": "Widget for displaying an object of fields for Netlify CMS.",
|
||||||
|
"version": "2.0.0-alpha.0",
|
||||||
|
"main": "dist/netlify-cms-widget-object.js",
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"netlify",
|
||||||
|
"netlify-cms",
|
||||||
|
"widget",
|
||||||
|
"fields",
|
||||||
|
"object",
|
||||||
|
"nested"
|
||||||
|
],
|
||||||
|
"sideEffects": false,
|
||||||
|
"scripts": {
|
||||||
|
"watch": "webpack -w",
|
||||||
|
"build": "webpack"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"webpack": "^4.16.1",
|
||||||
|
"webpack-cli": "^3.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"immutable": "^3.7.6",
|
||||||
|
"lodash": "^4.17.10",
|
||||||
|
"netlify-cms-ui-default": "^2.0.0-alpha.0",
|
||||||
|
"prop-types": "^15.5.10",
|
||||||
|
"react": "^16.4.1",
|
||||||
|
"react-emotion": "^9.2.6",
|
||||||
|
"react-immutable-proptypes": "^2.1.0"
|
||||||
|
}
|
||||||
|
}
|
@ -1,21 +1,19 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import styled, { css, cx } from 'react-emotion';
|
||||||
import { Map } from 'immutable';
|
import { Map } from 'immutable';
|
||||||
import { partial } from 'lodash';
|
import { partial } from 'lodash';
|
||||||
import c from 'classnames';
|
import { ObjectWidgetTopBar, Icon, colors, components } from 'netlify-cms-ui-default';
|
||||||
import { Icon } from 'netlify-cms-ui-default';
|
|
||||||
import { resolveWidget } from 'Lib/registry';
|
|
||||||
|
|
||||||
const TopBar = ({ collapsed, onCollapseToggle }) => (
|
const styles = {
|
||||||
<div className="nc-objectControl-topBar">
|
nestedObjectControl: css`
|
||||||
<div className="nc-objectControl-objectCollapseToggle">
|
padding: 6px 14px 14px;
|
||||||
<button className="nc-listControl-listCollapseToggleButton" onClick={onCollapseToggle}>
|
border-top: 0;
|
||||||
<Icon type="chevron" direction={collapsed ? 'right' : 'down'} size="small" />
|
border-top-left-radius: 0;
|
||||||
</button>
|
border-top-right-radius: 0;
|
||||||
</div>
|
`,
|
||||||
</div>
|
};
|
||||||
);
|
|
||||||
|
|
||||||
export default class ObjectControl extends Component {
|
export default class ObjectControl extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -35,6 +33,7 @@ export default class ObjectControl extends Component {
|
|||||||
classNameWrapper: PropTypes.string.isRequired,
|
classNameWrapper: PropTypes.string.isRequired,
|
||||||
forList: PropTypes.bool,
|
forList: PropTypes.bool,
|
||||||
editorControl: PropTypes.func.isRequired,
|
editorControl: PropTypes.func.isRequired,
|
||||||
|
resolveWidget: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@ -68,6 +67,7 @@ export default class ObjectControl extends Component {
|
|||||||
value,
|
value,
|
||||||
onChangeObject,
|
onChangeObject,
|
||||||
editorControl: EditorControl,
|
editorControl: EditorControl,
|
||||||
|
resolveWidget,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (field.get('widget') === 'hidden') {
|
if (field.get('widget') === 'hidden') {
|
||||||
@ -105,9 +105,15 @@ export default class ObjectControl extends Component {
|
|||||||
|
|
||||||
if (multiFields) {
|
if (multiFields) {
|
||||||
return (
|
return (
|
||||||
<div id={forID} className={c(classNameWrapper, 'nc-objectControl-root')}>
|
<div id={forID} className={cx(
|
||||||
{ forList ? null : <TopBar collapsed={collapsed} onCollapseToggle={this.handleCollapseToggle} /> }
|
classNameWrapper,
|
||||||
{ collapsed ? null : multiFields.map((f, idx) => this.controlFor(f, idx)) }
|
components.objectWidgetTopBarContainer,
|
||||||
|
{ [styles.nestedObjectControl]: forList },
|
||||||
|
)}>
|
||||||
|
{forList ? null :
|
||||||
|
<ObjectWidgetTopBar collapsed={collapsed} onCollapseToggle={this.handleCollapseToggle} />
|
||||||
|
}
|
||||||
|
{collapsed ? null : multiFields.map((f, idx) => this.controlFor(f, idx))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (singleField) {
|
} else if (singleField) {
|
13
packages/netlify-cms-widget-object/src/ObjectPreview.js
Normal file
13
packages/netlify-cms-widget-object/src/ObjectPreview.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { WidgetPreviewContainer } from 'netlify-cms-ui-default';
|
||||||
|
|
||||||
|
const ObjectPreview = ({ field }) => (
|
||||||
|
<WidgetPreviewContainer>{(field && field.get('fields')) || null}</WidgetPreviewContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
ObjectPreview.propTypes = {
|
||||||
|
field: PropTypes.node,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ObjectPreview;
|
2
packages/netlify-cms-widget-object/src/index.js
Normal file
2
packages/netlify-cms-widget-object/src/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export ObjectControl from './ObjectControl';
|
||||||
|
export ObjectPreview from './ObjectPreview';
|
3
packages/netlify-cms-widget-object/webpack.config.js
Normal file
3
packages/netlify-cms-widget-object/webpack.config.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
const { getConfig } = require('../../scripts/webpack.js');
|
||||||
|
|
||||||
|
module.exports = getConfig();
|
Loading…
x
Reference in New Issue
Block a user