Widget fixes (#162)

* Make string the default widget if none is specified

* Linting fixes for PreviewPane

* Linting fixes for ControlPane

* Add date widget

* Fix name of date control class

* Fix spaces in list control with no fields

* Fix linting error for List Control

* Fix linting errors in raw editor

* Add Select widget

* Fix linting error
This commit is contained in:
Mathias Biilmann 2016-11-17 04:08:37 -08:00 committed by Cássio Souza
parent 0338e5c9c2
commit d81d0d416f
12 changed files with 179 additions and 67 deletions

View File

@ -32,7 +32,10 @@
"git add"
]
},
"files": ["dist/", "README.md"],
"files": [
"dist/",
"README.md"
],
"pre-commit": "lint:staged",
"jest": {
"moduleNameMapper": {
@ -127,7 +130,7 @@
"react-datetime": "^2.6.0",
"react-dom": "^15.1.0",
"react-hot-loader": "^3.0.0-beta.2",
"react-immutable-proptypes": "^1.6.0",
"react-immutable-proptypes": "^2.1.0",
"react-lazy-load": "^3.0.3",
"react-portal": "^2.2.1",
"react-pure-render": "^1.0.2",

View File

@ -10,7 +10,7 @@ function isHidden(field) {
export default class ControlPane extends Component {
controlFor(field) {
const { entry, fields, getMedia, onChange, onAddMedia, onRemoveMedia } = this.props;
const { entry, getMedia, onChange, onAddMedia, onRemoveMedia } = this.props;
const widget = resolveWidget(field.get('widget'));
const fieldName = field.get('name');
const value = entry.getIn(['data', fieldName]);
@ -41,14 +41,12 @@ export default class ControlPane extends Component {
return (
<div>
{
fields.map(field =>
isHidden(field) ? null : <div
key={field.get('name')}
className={styles.widget}
>
{this.controlFor(field)}
</div>
)
fields.map((field) => {
if (isHidden(field)) {
return null;
}
return <div key={field.get('name')} className={styles.widget}>{this.controlFor(field)}</div>;
})
}
</div>
);

View File

@ -15,7 +15,7 @@ export default class PreviewPane extends React.Component {
widgetFor = (name) => {
const { fields, entry, getMedia } = this.props;
const field = fields.find(field => field.get('name') === name);
const field = fields.find(f => f.get('name') === name);
const widget = resolveWidget(field.get('widget'));
return React.createElement(widget.preview, {
key: field.get('name'),
@ -25,6 +25,21 @@ export default class PreviewPane extends React.Component {
});
};
handleIframeRef = (ref) => {
if (ref) {
registry.getPreviewStyles().forEach((style) => {
const linkEl = document.createElement('link');
linkEl.setAttribute('rel', 'stylesheet');
linkEl.setAttribute('href', style);
ref.contentDocument.head.appendChild(linkEl);
});
this.previewEl = document.createElement('div');
this.iframeBody = ref.contentDocument.body;
this.iframeBody.appendChild(this.previewEl);
this.renderPreview();
}
};
renderPreview() {
const { entry, collection } = this.props;
const component = registry.getPreviewTemplate(selectTemplateName(collection, entry.get('slug'))) || Preview;
@ -42,21 +57,6 @@ export default class PreviewPane extends React.Component {
, this.previewEl);
}
handleIframeRef = (ref) => {
if (ref) {
registry.getPreviewStyles().forEach((style) => {
const linkEl = document.createElement('link');
linkEl.setAttribute('rel', 'stylesheet');
linkEl.setAttribute('href', style);
ref.contentDocument.head.appendChild(linkEl);
});
this.previewEl = document.createElement('div');
this.iframeBody = ref.contentDocument.body;
this.iframeBody.appendChild(this.previewEl);
this.renderPreview();
}
};
render() {
const { collection } = this.props;
if (!collection) {
@ -72,7 +72,4 @@ PreviewPane.propTypes = {
fields: ImmutablePropTypes.list.isRequired,
entry: ImmutablePropTypes.map.isRequired,
getMedia: PropTypes.func.isRequired,
scrollTop: PropTypes.number,
scrollHeight: PropTypes.number,
offsetHeight: PropTypes.number,
};

View File

@ -13,8 +13,12 @@ import MarkdownControl from './Widgets/MarkdownControl';
import MarkdownPreview from './Widgets/MarkdownPreview';
import ImageControl from './Widgets/ImageControl';
import ImagePreview from './Widgets/ImagePreview';
import DateControl from './Widgets/DateControl';
import DatePreview from './Widgets/DatePreview';
import DateTimeControl from './Widgets/DateTimeControl';
import DateTimePreview from './Widgets/DateTimePreview';
import SelectControl from './Widgets/SelectControl';
import SelectPreview from './Widgets/SelectPreview';
import ObjectControl from './Widgets/ObjectControl';
import ObjectPreview from './Widgets/ObjectPreview';
@ -24,10 +28,12 @@ registry.registerWidget('number', NumberControl, NumberPreview);
registry.registerWidget('list', ListControl, ListPreview);
registry.registerWidget('markdown', MarkdownControl, MarkdownPreview);
registry.registerWidget('image', ImageControl, ImagePreview);
registry.registerWidget('date', DateControl, DatePreview);
registry.registerWidget('datetime', DateTimeControl, DateTimePreview);
registry.registerWidget('select', SelectControl, SelectPreview);
registry.registerWidget('object', ObjectControl, ObjectPreview);
registry.registerWidget('unknown', UnknownControl, UnknownPreview);
export function resolveWidget(name) {
return registry.getWidget(name) || registry.getWidget('unknown');
export function resolveWidget(name) { // eslint-disable-line
return registry.getWidget(name || 'string') || registry.getWidget('unknown');
}

View File

@ -0,0 +1,21 @@
import React, { PropTypes } from 'react';
import DateTime from 'react-datetime';
export default class DateControl extends React.Component {
handleChange = (datetime) => {
this.props.onChange(datetime);
};
render() {
return (<DateTime
timeFormat={false}
value={this.props.value || new Date()}
onChange={this.handleChange}
/>);
}
}
DateControl.propTypes = {
onChange: PropTypes.func.isRequired,
value: PropTypes.object,
};

View File

@ -0,0 +1,9 @@
import React, { PropTypes } from 'react';
export default function DatePreview({ value }) {
return <span>{value ? value.toString() : null}</span>;
}
DatePreview.propTypes = {
value: PropTypes.node,
};

View File

@ -13,6 +13,10 @@ ListItem.propTypes = {
};
ListItem.displayName = 'list-item';
function valueToString(value) {
return value ? value.join(',').replace(/,([^\s]|$)/g, ', $1') : '';
}
const SortableListItem = sortable(ListItem);
export default class ListControl extends Component {
@ -20,15 +24,32 @@ export default class ListControl extends Component {
onChange: PropTypes.func.isRequired,
value: PropTypes.node,
field: PropTypes.node,
getMedia: PropTypes.func.isRequired,
onAddMedia: PropTypes.func.isRequired,
onRemoveMedia: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
this.state = { itemStates: Map() };
this.state = { itemStates: Map(), value: valueToString(props.value) };
}
handleChange = (e) => {
this.props.onChange(e.target.value.split(',').map(item => item.trim()));
const oldValue = this.state.value;
const newValue = e.target.value;
const listValue = e.target.value.split(',');
if (newValue.match(/,$/) && oldValue.match(/, $/)) {
listValue.pop();
}
this.setState({ value: valueToString(listValue) });
this.props.onChange(listValue);
};
handleCleanup = (e) => {
const listValue = e.target.value.split(',').map(el => el.trim()).filter(el => el);
this.setState({ value: valueToString(listValue) });
this.props.onChange(listValue);
};
handleAdd = (e) => {
@ -80,13 +101,13 @@ export default class ListControl extends Component {
renderItem(item, index) {
const { value, field, getMedia, onAddMedia, onRemoveMedia } = this.props;
const { itemStates, draggedItem } = this.state;
const { itemStates } = this.state;
const collapsed = itemStates.getIn([index, 'collapsed']);
const classNames = [styles.item, collapsed ? styles.collapsed : styles.expanded];
return (<SortableListItem
key={index}
updateState={this.handleSort}
updateState={this.handleSort} // eslint-disable-line
items={value ? value.toJS() : []}
draggingIndex={this.state.draggingIndex}
sortId={index}
@ -113,7 +134,7 @@ export default class ListControl extends Component {
}
renderListControl() {
const { value, field } = this.props;
const { value } = this.props;
return (<div>
{value && value.map((item, index) => this.renderItem(item, index))}
<div><button className={styles.addButton} onClick={this.handleAdd}>new</button></div>
@ -121,12 +142,18 @@ export default class ListControl extends Component {
}
render() {
const { value, field } = this.props;
const { field } = this.props;
const { value } = this.state;
if (field.get('fields')) {
return this.renderListControl();
}
return <input type="text" value={value ? value.join(', ') : ''} onChange={this.handleChange} />;
return (<input
type="text"
value={value}
onChange={this.handleChange}
onBlur={this.handleCleanup}
/>);
}
}

View File

@ -1,5 +1,4 @@
import React, { PropTypes } from 'react';
import { fromJS } from 'immutable';
import MarkupIt from 'markup-it';
import markdownSyntax from 'markup-it/syntaxes/markdown';
import htmlSyntax from 'markup-it/syntaxes/html';
@ -65,29 +64,6 @@ function getCleanPaste(e) {
});
}
const buildtInPlugins = [{
label: 'Image',
id: 'image',
fromBlock: match => match && {
image: match[2],
alt: match[1],
},
toBlock: data => `![${ data.alt }](${ data.image })`,
toPreview: (data) => {
return <img src={data.image} alt={data.alt} />;
},
pattern: /^!\[([^\]]+)\]\(([^\)]+)\)$/,
fields: [{
label: 'Image',
name: 'image',
widget: 'image',
}, {
label: 'Alt Text',
name: 'alt',
}],
}];
buildtInPlugins.forEach(plugin => registry.registerEditorComponent(plugin));
export default class RawEditor extends React.Component {
constructor(props) {
super(props);
@ -237,7 +213,6 @@ export default class RawEditor extends React.Component {
if (selection.start !== selection.end && !HAS_LINE_BREAK.test(selection.selected)) {
try {
const selectionPosition = this.caretPosition.get(selection.start, selection.end);
console.log('pos: %o', selectionPosition);
this.setState({ showToolbar: true, showBlockMenu: false, selectionPosition });
} catch (e) {
this.setState({ showToolbar: false, showBlockMenu: false });

View File

@ -0,0 +1,44 @@
import React, { PropTypes } from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
export default class SelectControl extends React.Component {
handleChange = (e) => {
this.props.onChange(e.target.value);
};
render() {
const { field, value } = this.props;
const fieldOptions = field.get('options');
if (!fieldOptions) {
return <div>Error rendering select control for {field.get('name')}: No options</div>;
}
const options = fieldOptions.map((option) => {
if (typeof option === 'string') {
return { label: option, value: option };
}
return option;
});
return (<select value={value || ''} onChange={this.handleChange}>
{options.map((option, idx) => <option key={idx} value={option.value}>
{option.label}
</option>)}
</select>);
}
}
SelectControl.propTypes = {
onChange: PropTypes.func.isRequired,
value: PropTypes.node,
field: ImmutablePropTypes.contains({
options: ImmutablePropTypes.listOf(PropTypes.oneOf([
PropTypes.string,
ImmutablePropTypes.contains({
label: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
}),
])).isRequired,
}),
};

View File

@ -0,0 +1,9 @@
import React, { PropTypes } from 'react';
export default function SelectPreview({ value }) {
return <span>{value ? value.toString() : null}</span>;
}
SelectPreview.propTypes = {
value: PropTypes.string,
};

View File

@ -39,3 +39,26 @@ for (const method in registry) {
}
window.createClass = React.createClass;
window.h = React.createElement;
const buildtInPlugins = [{
label: 'Image',
id: 'image',
fromBlock: match => match && {
image: match[2],
alt: match[1],
},
toBlock: data => `![${ data.alt }](${ data.image })`,
toPreview: (data) => {
return <img src={data.image} alt={data.alt} />;
},
pattern: /^!\[([^\]]+)\]\(([^\)]+)\)$/,
fields: [{
label: 'Image',
name: 'image',
widget: 'image',
}, {
label: 'Alt Text',
name: 'alt',
}],
}];
buildtInPlugins.forEach(plugin => registry.registerEditorComponent(plugin));

View File

@ -6989,9 +6989,9 @@ react-hot-loader@^3.0.0-beta.2:
redbox-react "^1.2.5"
source-map "^0.4.4"
react-immutable-proptypes@^1.6.0:
version "1.7.2"
resolved "https://registry.yarnpkg.com/react-immutable-proptypes/-/react-immutable-proptypes-1.7.2.tgz#fb1fdca24e30501617732781f4341b704ef7c320"
react-immutable-proptypes:
version "2.1.0"
resolved "https://registry.yarnpkg.com/react-immutable-proptypes/-/react-immutable-proptypes-2.1.0.tgz#023d6f39bb15c97c071e9e60d00d136eac5fa0b4"
react-inspector@^1.1.0:
version "1.1.0"