Merge branch 'master' into markitup-react

This commit is contained in:
Andrey Okonetchnikov 2016-10-03 16:57:48 +02:00
commit ca34def49e
30 changed files with 392 additions and 466 deletions

View File

@ -4,7 +4,10 @@ env:
jest: true
parser: babel-eslint
plugins: [ "react" ]
plugins: [
"react",
"class-property"
]
rules:
# Possible Errors
@ -100,6 +103,8 @@ rules:
react/self-closing-comp: 1
react/sort-comp: 1
class-property/class-property-semicolon: 2
# Global scoped method and vars
globals:
netlify: true

22
LICENSE Normal file
View File

@ -0,0 +1,22 @@
Copyright (c) 2016 Netlify <team@netlify.com>
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -3,6 +3,9 @@
A CMS for static site generators. Give non-technical users a simple way to edit
and add content to any site built with a static site generator.
Netlify CMS is released under the [MIT License](LICENSE).
Please make sure you understand its [implications and guarantees](https://writing.kemitchell.com/2016/09/21/MIT-License-Line-by-Line.html).
## How it works
Netlify CMS is a single-page app that you pull into the `/admin` part of your site.

View File

@ -44,6 +44,7 @@
"css-loader": "^0.23.1",
"enzyme": "^2.4.1",
"eslint": "^3.5.0",
"eslint-plugin-class-property": "^1.0.1",
"eslint-plugin-react": "^5.1.1",
"expect": "^1.20.2",
"exports-loader": "^0.6.3",

View File

@ -6,13 +6,9 @@ export default class AuthenticationPage extends React.Component {
onLogin: React.PropTypes.func.isRequired
};
constructor(props) {
super(props);
this.state = {};
this.handleLogin = this.handleLogin.bind(this);
}
state = {};
handleLogin(e) {
handleLogin = e => {
e.preventDefault();
let auth;
if (document.location.host.split(':')[0] === 'localhost') {
@ -28,7 +24,7 @@ export default class AuthenticationPage extends React.Component {
}
this.props.onLogin(data);
});
}
};
render() {
const { loginError } = this.state;

View File

@ -5,13 +5,9 @@ export default class AuthenticationPage extends React.Component {
onLogin: React.PropTypes.func.isRequired
};
constructor(props) {
super(props);
this.state = {};
this.handleLogin = this.handleLogin.bind(this);
}
state = {};
handleLogin(e) {
handleLogin = e => {
e.preventDefault();
const { email, password } = this.state;
this.setState({ authenticating: true });
@ -33,7 +29,7 @@ export default class AuthenticationPage extends React.Component {
this.setState({ loginError: data.msg });
});
});
}
};
handleChange(key) {
return (e) => {

View File

@ -5,21 +5,16 @@ export default class AuthenticationPage extends React.Component {
onLogin: React.PropTypes.func.isRequired
};
constructor(props) {
super(props);
this.state = { email: '' };
this.handleLogin = this.handleLogin.bind(this);
this.handleEmailChange = this.handleEmailChange.bind(this);
}
state = { email: '' };
handleLogin(e) {
handleLogin = e => {
e.preventDefault();
this.props.onLogin(this.state);
}
};
handleEmailChange(e) {
handleEmailChange = e => {
this.setState({ email: e.target.value });
}
};
render() {
return <form onSubmit={this.handleLogin}>

View File

@ -10,26 +10,26 @@ export default class AppHeader extends React.Component {
state = {
createMenuActive: false
}
};
handleCreatePostClick = collectionName => {
const { onCreateEntryClick } = this.props;
if (onCreateEntryClick) {
onCreateEntryClick(collectionName);
}
}
};
handleCreateButtonClick = () => {
this.setState({
createMenuActive: true
});
}
};
handleCreateMenuHide = () => {
this.setState({
createMenuActive: false
});
}
};
render() {
const {
@ -43,41 +43,41 @@ export default class AppHeader extends React.Component {
return (
<AppBar
fixed
theme={styles}
fixed
theme={styles}
>
<IconButton
icon="menu"
inverse
onClick={toggleNavDrawer}
icon="menu"
inverse
onClick={toggleNavDrawer}
/>
<IndexLink to="/">
Dashboard
</IndexLink>
<FindBar
commands={commands}
defaultCommands={defaultCommands}
runCommand={runCommand}
commands={commands}
defaultCommands={defaultCommands}
runCommand={runCommand}
/>
<Button
className={styles.createBtn}
icon='add'
floating
accent
onClick={this.handleCreateButtonClick}
className={styles.createBtn}
icon='add'
floating
accent
onClick={this.handleCreateButtonClick}
>
<Menu
active={createMenuActive}
position="topRight"
onHide={this.handleCreateMenuHide}
active={createMenuActive}
position="topRight"
onHide={this.handleCreateMenuHide}
>
{
collections.valueSeq().map(collection =>
<MenuItem
key={collection.get('name')}
value={collection.get('name')}
onClick={this.handleCreatePostClick.bind(this, collection.get('name'))}
caption={pluralize(collection.get('label'), 1)}
key={collection.get('name')}
value={collection.get('name')}
onClick={this.handleCreatePostClick.bind(this, collection.get('name'))}
caption={pluralize(collection.get('label'), 1)}
/>
)
}

View File

@ -17,9 +17,8 @@ export default class EntryListing extends React.Component {
{ mq: '495px', columns: 2, gutter: 15 },
{ mq: '750px', columns: 3, gutter: 15 },
{ mq: '1005px', columns: 4, gutter: 15 },
{ mq: '1260px', columns: 5, gutter: 15 },
{ mq: '1515px', columns: 6, gutter: 15 },
{ mq: '1770px', columns: 7, gutter: 15 },
{ mq: '1515px', columns: 5, gutter: 15 },
{ mq: '1770px', columns: 6, gutter: 15 },
]
};

View File

@ -8,8 +8,18 @@ export const SEARCH = 'SEARCH';
const PLACEHOLDER = 'Search or enter a command';
class FindBar extends Component {
constructor(props) {
super(props);
static propTypes = {
commands: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
pattern: PropTypes.string.isRequired
})).isRequired,
defaultCommands: PropTypes.arrayOf(PropTypes.string),
runCommand: PropTypes.func.isRequired,
};
constructor() {
super();
this._compiledCommands = [];
this._searchCommand = {
search: true,
@ -26,18 +36,6 @@ class FindBar extends Component {
};
this._getSuggestions = _.memoize(this._getSuggestions, (value, activeScope) => value + activeScope);
this.compileCommand = this.compileCommand.bind(this);
this.matchCommand = this.matchCommand.bind(this);
this.maybeRemoveActiveScope = this.maybeRemoveActiveScope.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleInputBlur = this.handleInputBlur.bind(this);
this.handleInputFocus = this.handleInputFocus.bind(this);
this.handleInputClick = this.handleInputClick.bind(this);
this.getSuggestions = this.getSuggestions.bind(this);
this.highlightCommandFromMouse = this.highlightCommandFromMouse.bind(this);
this.selectCommandFromMouse = this.selectCommandFromMouse.bind(this);
this.setIgnoreBlur = this.setIgnoreBlur.bind(this);
}
componentWillMount() {
@ -58,7 +56,7 @@ class FindBar extends Component {
}
// Generates a regexp and splits a token and param details for a command
compileCommand(command) {
compileCommand = command => {
let regexp = '';
let param = null;
@ -79,11 +77,11 @@ class FindBar extends Component {
token,
param
});
}
};
// Check if the entered string matches any command.
// adds a scope (so user can type param value) and dispatches action for fully matched commands
matchCommand() {
matchCommand = () => {
const string = this.state.activeScope ? this.state.activeScope + this.state.value : this.state.value;
let match;
let command = this._compiledCommands.find(command => {
@ -133,20 +131,20 @@ class FindBar extends Component {
}
this.props.runCommand(command.type, payload);
}
}
};
maybeRemoveActiveScope() {
maybeRemoveActiveScope = () => {
if (this.state.value.length === 0 && this.state.activeScope) {
this.setState({
activeScope: null,
placeholder: PLACEHOLDER
});
}
}
};
getSuggestions() {
getSuggestions = () => {
return this._getSuggestions(this.state.value, this.state.activeScope, this._compiledCommands, this.props.defaultCommands);
}
};
// Memoized version
_getSuggestions(value, scope, commands, defaultCommands) {
@ -173,7 +171,7 @@ class FindBar extends Component {
return returnResults;
}
handleKeyDown(event) {
handleKeyDown = event => {
let highlightedIndex, index;
switch (event.key) {
case 'ArrowDown':
@ -240,37 +238,37 @@ class FindBar extends Component {
isOpen: true
});
}
}
};
handleChange(event) {
handleChange = event => {
this.setState({
value: event.target.value,
});
}
};
handleInputBlur() {
handleInputBlur = () => {
if (this._ignoreBlur) return;
this.setState({
isOpen: false,
highlightedIndex: 0
});
}
};
handleInputFocus() {
handleInputFocus = () => {
if (this._ignoreBlur) return;
this.setState({ isOpen: true });
}
};
handleInputClick() {
handleInputClick = () => {
if (this.state.isOpen === false)
this.setState({ isOpen: true });
}
};
highlightCommandFromMouse(index) {
highlightCommandFromMouse = index => {
this.setState({ highlightedIndex: index });
}
};
selectCommandFromMouse(command) {
selectCommandFromMouse = command => {
const newState = {
isOpen: false,
highlightedIndex: 0
@ -283,11 +281,11 @@ class FindBar extends Component {
this._input.focus();
this.setIgnoreBlur(false);
});
}
};
setIgnoreBlur(ignore) {
setIgnoreBlur = ignore => {
this._ignoreBlur = ignore;
}
};
renderMenu() {
const commands = this.getSuggestions().map((command, index) => {
@ -309,11 +307,11 @@ class FindBar extends Component {
}
return (
<div
className={this.state.highlightedIndex === index ? styles.highlightedCommand : styles.command}
key={command.token.trim().replace(/[^a-z0-9]+/gi, '-')}
onMouseDown={() => this.setIgnoreBlur(true)}
onMouseEnter={() => this.highlightCommandFromMouse(index)}
onClick={() => this.selectCommandFromMouse(command)}
className={this.state.highlightedIndex === index ? styles.highlightedCommand : styles.command}
key={command.token.trim().replace(/[^a-z0-9]+/gi, '-')}
onMouseDown={() => this.setIgnoreBlur(true)}
onMouseEnter={() => this.highlightCommandFromMouse(index)}
onClick={() => this.selectCommandFromMouse(command)}
>
{children}
</div>
@ -347,15 +345,15 @@ class FindBar extends Component {
<label className={styles.inputArea}>
{scope}
<input
className={styles.inputField}
ref={(c) => this._input = c}
onFocus={this.handleInputFocus}
onBlur={this.handleInputBlur}
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
onClick={this.handleInputClick}
placeholder={this.state.placeholder}
value={this.state.value}
className={styles.inputField}
ref={(c) => this._input = c}
onFocus={this.handleInputFocus}
onBlur={this.handleInputBlur}
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
onClick={this.handleInputClick}
placeholder={this.state.placeholder}
value={this.state.value}
/>
</label>
{menu}
@ -364,14 +362,4 @@ class FindBar extends Component {
}
}
FindBar.propTypes = {
commands: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
pattern: PropTypes.string.isRequired
})).isRequired,
defaultCommands: PropTypes.arrayOf(PropTypes.string),
runCommand: PropTypes.func.isRequired,
};
export default FindBar;

View File

@ -3,14 +3,10 @@ import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import styles from './Loader.css';
export default class Loader extends React.Component {
constructor(props) {
super(props);
this.state = {
currentItem: 0,
};
this.setAnimation = this.setAnimation.bind(this);
this.renderChild = this.renderChild.bind(this);
}
state = {
currentItem: 0,
};
componentWillUnmount() {
if (this.interval) {
@ -18,7 +14,7 @@ export default class Loader extends React.Component {
}
}
setAnimation() {
setAnimation = () => {
if (this.interval) return;
const { children } = this.props;
@ -27,9 +23,9 @@ export default class Loader extends React.Component {
const nextItem = (this.state.currentItem === children.length - 1) ? 0 : this.state.currentItem + 1;
this.setState({ currentItem: nextItem });
}, 5000);
}
};
renderChild() {
renderChild = () => {
const { children } = this.props;
const { currentItem } = this.state;
if (!children) {
@ -40,15 +36,15 @@ export default class Loader extends React.Component {
this.setAnimation();
return <div className={styles.text}>
<ReactCSSTransitionGroup
transitionName={styles}
transitionEnterTimeout={500}
transitionLeaveTimeout={500}
transitionName={styles}
transitionEnterTimeout={500}
transitionLeaveTimeout={500}
>
<div key={currentItem} className={styles.animateItem}>{children[currentItem]}</div>
</ReactCSSTransitionGroup>
</div>;
}
}
};
render() {
const { active, style, className = '' } = this.props;

View File

@ -3,14 +3,10 @@ import { Icon } from '../index';
import styles from './Toast.css';
export default class Toast extends React.Component {
constructor(props) {
super(props);
this.state = {
shown: false
};
this.autoHideTimeout = this.autoHideTimeout.bind(this);
}
state = {
shown: false
};
componentWillMount() {
if (this.props.show) {
@ -32,12 +28,12 @@ export default class Toast extends React.Component {
}
}
autoHideTimeout() {
autoHideTimeout = () => {
clearTimeout(this.timeOut);
this.timeOut = setTimeout(() => {
this.setState({ shown: false });
}, 4000);
}
};
render() {
const { style, type, className, children } = this.props;

View File

@ -24,25 +24,26 @@
.card {
width: 100% !important;
margin: 7px 0;
margin: 7px 0 0 10px;
padding: 7px 0;
}
& h2 {
font-size: 17px;
& small {
font-weight: normal;
}
.cardHeading {
font-size: 17px;
& small {
font-weight: normal;
}
}
& p {
color: #555;
font-size: 12px;
margin-top: 5px;
}
.cardText {
color: #555;
font-size: 12px;
margin-top: 5px;
}
& button {
margin: 10px 10px 0 0;
float: right;
}
.button {
margin: 10px 10px 0 0;
float: right;
}

View File

@ -8,28 +8,21 @@ import { status, statusDescriptions } from '../constants/publishModes';
import styles from './UnpublishedListing.css';
class UnpublishedListing extends React.Component {
constructor(props) {
super(props);
this.renderColumns = this.renderColumns.bind(this);
this.handleChangeStatus = this.handleChangeStatus.bind(this);
this.requestPublish = this.requestPublish.bind(this);
}
handleChangeStatus(newStatus, dragProps) {
handleChangeStatus = (newStatus, dragProps) => {
const slug = dragProps.slug;
const collection = dragProps.collection;
const oldStatus = dragProps.ownStatus;
this.props.handleChangeStatus(collection, slug, oldStatus, newStatus);
}
};
requestPublish(collection, slug, ownStatus) {
requestPublish = (collection, slug, ownStatus) => {
if (ownStatus !== status.last()) return;
if (window.confirm('Are you sure you want to publish this entry?')) {
this.props.handlePublish(collection, slug, ownStatus);
}
}
};
renderColumns(entries, column) {
renderColumns = (entries, column) => {
if (!entries) return;
if (!column) {
@ -60,10 +53,10 @@ class UnpublishedListing extends React.Component {
<DragSource key={slug} slug={slug} collection={collection} ownStatus={ownStatus}>
<div className={styles.drag}>
<Card className={styles.card}>
<h2><Link to={link}>{entry.getIn(['data', 'title'])}</Link> <small>by {author}</small></h2>
<p>Last updated: {timeStamp} by {entry.getIn(['metaData', 'user'])}</p>
<span className={styles.cardHeading}><Link to={link}>{entry.getIn(['data', 'title'])}</Link> <small>by {author}</small></span>
<p className={styles.cardText}>Last updated: {timeStamp} by {entry.getIn(['metaData', 'user'])}</p>
{(ownStatus === status.last()) &&
<button onClick={this.requestPublish.bind(this, collection, slug, status)}>Publish now</button>
<button className={styles.button} onClick={this.requestPublish.bind(this, collection, slug, status)}>Publish now</button>
}
</Card>
</div>
@ -74,7 +67,13 @@ class UnpublishedListing extends React.Component {
)}
</div>;
}
}
};
static propTypes = {
entries: ImmutablePropTypes.orderedMap,
handleChangeStatus: PropTypes.func.isRequired,
handlePublish: PropTypes.func.isRequired,
};
render() {
const columns = this.renderColumns(this.props.entries);
@ -89,10 +88,4 @@ class UnpublishedListing extends React.Component {
}
}
UnpublishedListing.propTypes = {
entries: ImmutablePropTypes.orderedMap,
handleChangeStatus: PropTypes.func.isRequired,
handlePublish: PropTypes.func.isRequired,
};
export default HTML5DragDrop(UnpublishedListing);

View File

@ -2,14 +2,9 @@ import React, { PropTypes } from 'react';
import DateTime from 'react-datetime';
export default class DateTimeControl extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(datetime) {
handleChange = datetime => {
this.props.onChange(datetime);
}
};
render() {
return <DateTime value={this.props.value || new Date()} onChange={this.handleChange}/>;

View File

@ -5,36 +5,25 @@ import MediaProxy from '../../valueObjects/MediaProxy';
const MAX_DISPLAY_LENGTH = 50;
export default class ImageControl extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleFileInputRef = this.handleFileInputRef.bind(this);
this.handleClick = this.handleClick.bind(this);
this.handleDragEnter = this.handleDragEnter.bind(this);
this.handleDragOver = this.handleDragOver.bind(this);
this.renderImageName = this.renderImageName.bind(this);
}
handleFileInputRef(el) {
handleFileInputRef = el => {
this._fileInput = el;
}
};
handleClick(e) {
handleClick = e => {
this._fileInput.click();
}
};
handleDragEnter(e) {
handleDragEnter = e => {
e.stopPropagation();
e.preventDefault();
}
};
handleDragOver(e) {
handleDragOver = e => {
e.stopPropagation();
e.preventDefault();
}
};
handleChange(e) {
handleChange = e => {
e.stopPropagation();
e.preventDefault();
@ -58,9 +47,9 @@ export default class ImageControl extends React.Component {
this.props.onChange(null);
}
}
};
renderImageName() {
renderImageName = () => {
if (!this.props.value) return null;
if (this.value instanceof MediaProxy) {
return truncateMiddle(this.props.value.path, MAX_DISPLAY_LENGTH);
@ -68,25 +57,25 @@ export default class ImageControl extends React.Component {
return truncateMiddle(this.props.value, MAX_DISPLAY_LENGTH);
}
}
};
render() {
const imageName = this.renderImageName();
return (
<div
onDragEnter={this.handleDragEnter}
onDragOver={this.handleDragOver}
onDrop={this.handleChange}
onDragEnter={this.handleDragEnter}
onDragOver={this.handleDragOver}
onDrop={this.handleChange}
>
<span style={styles.imageUpload} onClick={this.handleClick}>
{imageName ? imageName : 'Tip: Click here to upload an image from your file browser, or drag an image directly into this box from your desktop'}
</span>
<input
type="file"
accept="image/*"
onChange={this.handleChange}
style={styles.input}
ref={this.handleFileInputRef}
type="file"
accept="image/*"
onChange={this.handleChange}
style={styles.input}
ref={this.handleFileInputRef}
/>
</div>
);

View File

@ -7,6 +7,14 @@ import { connect } from 'react-redux';
import { switchVisualMode } from '../../actions/editor';
class MarkdownControl extends React.Component {
static propTypes = {
editor: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
onAddMedia: PropTypes.func.isRequired,
getMedia: PropTypes.func.isRequired,
switchVisualMode: PropTypes.func.isRequired,
value: PropTypes.node,
};
componentWillMount() {
this.useRawEditor();
@ -15,11 +23,11 @@ class MarkdownControl extends React.Component {
useVisualEditor = () => {
this.props.switchVisualMode(true);
}
};
useRawEditor = () => {
this.props.switchVisualMode(false);
}
};
render() {
const { editor, onChange, onAddMedia, getMedia, value } = this.props;
@ -28,11 +36,11 @@ class MarkdownControl extends React.Component {
<div className='cms-editor-visual'>
{null && <button onClick={this.useRawEditor}>Switch to Raw Editor</button>}
<VisualEditor
onChange={onChange}
onAddMedia={onAddMedia}
getMedia={getMedia}
registeredComponents={editor.get('registeredComponents')}
value={value}
onChange={onChange}
onAddMedia={onAddMedia}
getMedia={getMedia}
registeredComponents={editor.get('registeredComponents')}
value={value}
/>
</div>
);
@ -41,10 +49,10 @@ class MarkdownControl extends React.Component {
<div className='cms-editor-raw'>
{null && <button onClick={this.useVisualEditor}>Switch to Visual Editor</button>}
<RawEditor
onChange={onChange}
onAddMedia={onAddMedia}
getMedia={getMedia}
value={value}
onChange={onChange}
onAddMedia={onAddMedia}
getMedia={getMedia}
value={value}
/>
</div>
);
@ -52,19 +60,6 @@ class MarkdownControl extends React.Component {
}
}
MarkdownControl.propTypes = {
editor: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
onAddMedia: PropTypes.func.isRequired,
getMedia: PropTypes.func.isRequired,
switchVisualMode: PropTypes.func.isRequired,
value: PropTypes.node,
};
MarkdownControl.contextTypes = {
plugins: PropTypes.object,
};
export default connect(
state => ({ editor: state.editor }),
{ switchVisualMode }

View File

@ -72,7 +72,14 @@ const SCHEMA = {
}
};
class RawEditor extends React.Component {
export default class RawEditor extends React.Component {
static propTypes = {
onAddMedia: PropTypes.func.isRequired,
getMedia: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
value: PropTypes.string,
};
constructor(props) {
super(props);
@ -104,12 +111,12 @@ class RawEditor extends React.Component {
*/
handleChange = state => {
this.setState({ state });
}
};
handleDocumentChange = (document, state) => {
const content = Plain.serialize(state, { terse: true });
this.props.onChange(content);
}
};
render() {
return (
@ -125,12 +132,3 @@ class RawEditor extends React.Component {
);
}
}
export default RawEditor;
RawEditor.propTypes = {
onAddMedia: PropTypes.func.isRequired,
getMedia: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
value: PropTypes.string,
};

View File

@ -5,21 +5,17 @@ import MediaProxy from '../../../../valueObjects/MediaProxy';
import styles from './BlockTypesMenu.css';
class BlockTypesMenu extends Component {
constructor(props) {
super(props);
this.state = {
expanded: false
};
static propTypes = {
plugins: PropTypes.array.isRequired,
onClickBlock: PropTypes.func.isRequired,
onClickPlugin: PropTypes.func.isRequired,
onClickImage: PropTypes.func.isRequired
};
this.toggleMenu = this.toggleMenu.bind(this);
this.handleBlockTypeClick = this.handleBlockTypeClick.bind(this);
this.handlePluginClick = this.handlePluginClick.bind(this);
this.handleFileUploadClick = this.handleFileUploadClick.bind(this);
this.handleFileUploadChange = this.handleFileUploadChange.bind(this);
this.renderBlockTypeButton = this.renderBlockTypeButton.bind(this);
this.renderPluginButton = this.renderPluginButton.bind(this);
}
state = {
expanded: false
};
componentWillUpdate() {
if (this.state.expanded) {
@ -27,27 +23,27 @@ class BlockTypesMenu extends Component {
}
}
toggleMenu() {
toggleMenu = () => {
this.setState({ expanded: !this.state.expanded });
}
};
handleBlockTypeClick(e, type) {
handleBlockTypeClick = (e, type) => {
this.props.onClickBlock(type);
}
};
handlePluginClick(e, plugin) {
handlePluginClick = (e, plugin) => {
const data = {};
plugin.fields.forEach(field => {
data[field.name] = window.prompt(field.label); // eslint-disable-line
});
this.props.onClickPlugin(plugin.id, data);
}
};
handleFileUploadClick() {
handleFileUploadClick = () => {
this._fileInput.click();
}
};
handleFileUploadChange(e) {
handleFileUploadChange = e => {
e.stopPropagation();
e.preventDefault();
@ -67,21 +63,21 @@ class BlockTypesMenu extends Component {
this.props.onClickImage(mediaProxy);
}
}
};
renderBlockTypeButton(type, icon) {
renderBlockTypeButton = (type, icon) => {
const onClick = e => this.handleBlockTypeClick(e, type);
return (
<Icon key={type} type={icon} onClick={onClick} className={styles.icon}/>
);
}
};
renderPluginButton(plugin) {
renderPluginButton = plugin => {
const onClick = e => this.handlePluginClick(e, plugin);
return (
<Icon key={plugin.id} type={plugin.icon} onClick={onClick} className={styles.icon}/>
);
}
};
renderMenu() {
const { plugins } = this.props;
@ -117,11 +113,4 @@ class BlockTypesMenu extends Component {
}
}
BlockTypesMenu.propTypes = {
plugins: PropTypes.array.isRequired,
onClickBlock: PropTypes.func.isRequired,
onClickPlugin: PropTypes.func.isRequired,
onClickImage: PropTypes.func.isRequired
};
export default withPortalAtCursorPosition(BlockTypesMenu);

View File

@ -5,43 +5,39 @@ import styles from './StylesMenu.css';
class StylesMenu extends Component {
constructor() {
super();
this.hasMark = this.hasMark.bind(this);
this.hasBlock = this.hasBlock.bind(this);
this.renderMarkButton = this.renderMarkButton.bind(this);
this.renderBlockButton = this.renderBlockButton.bind(this);
this.renderLinkButton = this.renderLinkButton.bind(this);
this.handleMarkClick = this.handleMarkClick.bind(this);
this.handleInlineClick = this.handleInlineClick.bind(this);
this.handleBlockClick = this.handleBlockClick.bind(this);
}
static propTypes = {
marks: PropTypes.object.isRequired,
blocks: PropTypes.object.isRequired,
inlines: PropTypes.object.isRequired,
onClickBlock: PropTypes.func.isRequired,
onClickMark: PropTypes.func.isRequired,
onClickInline: PropTypes.func.isRequired
};
/**
* Used to set toolbar buttons to active state
*/
hasMark(type) {
hasMark = type => {
const { marks } = this.props;
return marks.some(mark => mark.type == type);
}
};
hasBlock(type) {
hasBlock = type => {
const { blocks } = this.props;
return blocks.some(node => node.type == type);
}
};
hasLinks(type) {
hasLinks = type => {
const { inlines } = this.props;
return inlines.some(inline => inline.type == 'link');
}
};
handleMarkClick(e, type) {
handleMarkClick = (e, type) => {
e.preventDefault();
this.props.onClickMark(type);
}
};
renderMarkButton(type, icon) {
renderMarkButton = (type, icon) => {
const isActive = this.hasMark(type);
const onMouseDown = e => this.handleMarkClick(e, type);
return (
@ -49,14 +45,14 @@ class StylesMenu extends Component {
<Icon type={icon}/>
</span>
);
}
};
handleInlineClick(e, type, isActive) {
handleInlineClick = (e, type, isActive) => {
e.preventDefault();
this.props.onClickInline(type, isActive);
}
};
renderLinkButton() {
renderLinkButton = () => {
const isActive = this.hasLinks();
const onMouseDown = e => this.handleInlineClick(e, 'link', isActive);
return (
@ -64,16 +60,16 @@ class StylesMenu extends Component {
<Icon type="link"/>
</span>
);
}
};
handleBlockClick(e, type) {
handleBlockClick = (e, type) => {
e.preventDefault();
const isActive = this.hasBlock(type);
const isList = this.hasBlock('list-item');
this.props.onClickBlock(type, isActive, isList);
}
};
renderBlockButton(type, icon, checkType) {
renderBlockButton = (type, icon, checkType) => {
checkType = checkType || type;
const isActive = this.hasBlock(checkType);
const onMouseDown = e => this.handleBlockClick(e, type);
@ -82,7 +78,7 @@ class StylesMenu extends Component {
<Icon type={icon}/>
</span>
);
}
};
render() {
return (
@ -98,16 +94,6 @@ class StylesMenu extends Component {
</div>
);
}
}
StylesMenu.propTypes = {
marks: PropTypes.object.isRequired,
blocks: PropTypes.object.isRequired,
inlines: PropTypes.object.isRequired,
onClickBlock: PropTypes.func.isRequired,
onClickMark: PropTypes.func.isRequired,
onClickInline: PropTypes.func.isRequired
};
export default withPortalAtCursorPosition(StylesMenu);

View File

@ -13,11 +13,18 @@ import BlockTypesMenu from './BlockTypesMenu';
/**
* Slate Render Configuration
*/
class VisualEditor extends React.Component {
export default class VisualEditor extends React.Component {
static propTypes = {
onChange: PropTypes.func.isRequired,
onAddMedia: PropTypes.func.isRequired,
getMedia: PropTypes.func.isRequired,
value: PropTypes.string,
};
constructor(props) {
super(props);
this.getMedia = this.getMedia.bind(this);
const MarkdownSyntax = getSyntaxes(this.getMedia).markdown;
this.markdown = new MarkupIt(MarkdownSyntax);
@ -46,48 +53,36 @@ class VisualEditor extends React.Component {
}
})
];
this.handleChange = this.handleChange.bind(this);
this.handleDocumentChange = this.handleDocumentChange.bind(this);
this.handleMarkStyleClick = this.handleMarkStyleClick.bind(this);
this.handleBlockStyleClick = this.handleBlockStyleClick.bind(this);
this.handleInlineClick = this.handleInlineClick.bind(this);
this.handleBlockTypeClick = this.handleBlockTypeClick.bind(this);
this.handlePluginClick = this.handlePluginClick.bind(this);
this.handleImageClick = this.handleImageClick.bind(this);
this.focusAndAddParagraph = this.focusAndAddParagraph.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.renderBlockTypesMenu = this.renderBlockTypesMenu.bind(this);
}
getMedia(src) {
getMedia = src => {
return this.props.getMedia(src);
}
};
/**
* Slate keeps track of selections, scroll position etc.
* So, onChange gets dispatched on every interaction (click, arrows, everything...)
* It also have an onDocumentChange, that get's dispached only when the actual
* It also have an onDocumentChange, that get's dispatched only when the actual
* content changes
*/
handleChange(state) {
handleChange = state => {
if (this.blockEdit) {
this.blockEdit = false;
} else {
this.setState({ state });
}
}
};
handleDocumentChange(document, state) {
handleDocumentChange = (document, state) => {
const rawJson = Raw.serialize(state, { terse: true });
const content = SlateUtils.decode(rawJson);
this.props.onChange(this.markdown.toText(content));
}
};
/**
* Toggle marks / blocks when button is clicked
*/
handleMarkStyleClick(type) {
handleMarkStyleClick = type => {
let { state } = this.state;
state = state
@ -96,9 +91,9 @@ class VisualEditor extends React.Component {
.apply();
this.setState({ state });
}
};
handleBlockStyleClick(type, isActive, isList) {
handleBlockStyleClick = (type, isActive, isList) => {
let { state } = this.state;
let transform = state.transform();
const { document } = state;
@ -142,7 +137,7 @@ class VisualEditor extends React.Component {
state = transform.apply();
this.setState({ state });
}
};
/**
* When clicking a link, if the selection has a link in it, remove the link.
@ -151,7 +146,7 @@ class VisualEditor extends React.Component {
* @param {Event} e
*/
handleInlineClick(type, isActive) {
handleInlineClick = (type, isActive) => {
let { state } = this.state;
if (type === 'link') {
@ -177,9 +172,9 @@ class VisualEditor extends React.Component {
}
}
this.setState({ state });
}
};
handleBlockTypeClick(type) {
handleBlockTypeClick = type => {
let { state } = this.state;
state = state
@ -191,9 +186,9 @@ class VisualEditor extends React.Component {
.apply();
this.setState({ state }, this.focusAndAddParagraph);
}
};
handlePluginClick(type, data) {
handlePluginClick = (type, data) => {
let { state } = this.state;
state = state
@ -209,9 +204,9 @@ class VisualEditor extends React.Component {
.apply();
this.setState({ state });
}
};
handleImageClick(mediaProxy) {
handleImageClick = mediaProxy => {
let { state } = this.state;
this.props.onAddMedia(mediaProxy);
@ -221,9 +216,9 @@ class VisualEditor extends React.Component {
.apply();
this.setState({ state });
}
};
focusAndAddParagraph() {
focusAndAddParagraph = () => {
const { state } = this.state;
const blocks = state.document.getBlocks();
const last = blocks.last();
@ -237,9 +232,9 @@ class VisualEditor extends React.Component {
snapshot: false
});
this.setState({ state: normalized });
}
};
handleKeyDown(evt) {
handleKeyDown = evt => {
if (evt.shiftKey && evt.key === 'Enter') {
this.blockEdit = true;
let { state } = this.state;
@ -250,9 +245,9 @@ class VisualEditor extends React.Component {
this.setState({ state });
}
}
};
renderBlockTypesMenu() {
renderBlockTypesMenu = () => {
const currentBlock = this.state.state.blocks.get(0);
const isOpen = (this.props.value !== undefined && currentBlock.isEmpty && currentBlock.type !== 'horizontal-rule');
@ -265,7 +260,7 @@ class VisualEditor extends React.Component {
onClickImage={this.handleImageClick}
/>
);
}
};
renderStylesMenu() {
const { state } = this.state;
@ -302,12 +297,3 @@ class VisualEditor extends React.Component {
);
}
}
export default VisualEditor;
VisualEditor.propTypes = {
onChange: PropTypes.func.isRequired,
onAddMedia: PropTypes.func.isRequired,
getMedia: PropTypes.func.isRequired,
value: PropTypes.string,
};

View File

@ -18,6 +18,14 @@ const EditorComponent = Record({
class Plugin extends Component {
static propTypes = {
children: PropTypes.element.isRequired
};
static childContextTypes = {
plugins: PropTypes.object
};
getChildContext() {
return { plugins: plugins };
}
@ -27,13 +35,6 @@ class Plugin extends Component {
}
}
Plugin.propTypes = {
children: PropTypes.element.isRequired
};
Plugin.childContextTypes = {
plugins: PropTypes.object
};
export function newEditorPlugin(config) {
const configObj = new EditorComponent({
id: config.id || config.label.replace(/[^A-Z0-9]+/ig, '_'),

View File

@ -1,14 +1,9 @@
import React, { PropTypes } from 'react';
export default class StringControl extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
handleChange = e => {
this.props.onChange(e.target.value);
}
};
render() {
return <input type="text" value={this.props.value || ''} onChange={this.handleChange}/>;

View File

@ -1,20 +1,14 @@
import React, { PropTypes } from 'react';
export default class StringControl extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleRef = this.handleRef.bind(this);
}
componentDidMount() {
this.updateHeight();
}
handleChange(e) {
handleChange = e => {
this.props.onChange(e.target.value);
this.updateHeight();
}
};
updateHeight() {
if (this.element.scrollHeight > this.element.clientHeight) {
@ -22,9 +16,9 @@ export default class StringControl extends React.Component {
}
}
handleRef(ref) {
handleRef = ref => {
this.element = ref;
}
};
render() {
return <textarea ref={this.handleRef} value={this.props.value || ''} onChange={this.handleChange}/>;

View File

@ -4,7 +4,17 @@
.nav {
display: block;
padding: 1rem;
& .heading {
border: none;
}
}
.main {
padding-top: 54px;
}
.navDrawer {
max-width: 240px !important;
& .drawerContent {
max-width: 240px !important;
}
}

View File

@ -20,8 +20,8 @@ import styles from './App.css';
class App extends React.Component {
state = {
navDrawerIsVisible: false
}
navDrawerIsVisible: true
};
componentDidMount() {
this.props.dispatch(loadConfig());
@ -100,7 +100,7 @@ class App extends React.Component {
this.setState({
navDrawerIsVisible: !this.state.navDrawerIsVisible
});
}
};
render() {
const { navDrawerIsVisible } = this.state;
@ -135,18 +135,19 @@ class App extends React.Component {
return (
<Layout theme={styles}>
<NavDrawer
active={navDrawerIsVisible}
scrollY
permanentAt="md"
active={navDrawerIsVisible}
scrollY
permanentAt={navDrawerIsVisible ? 'lg' : null}
theme={styles}
>
<nav className={styles.nav}>
<h1>Collections</h1>
<h1 className={styles.heading}>Collections</h1>
<Navigation type='vertical'>
{
collections.valueSeq().map(collection =>
<Link
key={collection.get('name')}
onClick={navigateToCollection.bind(this, collection.get('name'))}
key={collection.get('name')}
onClick={navigateToCollection.bind(this, collection.get('name'))}
>
{collection.get('label')}
</Link>
@ -157,14 +158,14 @@ class App extends React.Component {
</NavDrawer>
<Panel scrollY>
<AppHeader
collections={collections}
commands={commands}
defaultCommands={defaultCommands}
runCommand={runCommand}
onCreateEntryClick={createNewEntryInCollection}
toggleNavDrawer={this.toggleNavDrawer}
collections={collections}
commands={commands}
defaultCommands={defaultCommands}
runCommand={runCommand}
onCreateEntryClick={createNewEntryInCollection}
toggleNavDrawer={this.toggleNavDrawer}
/>
<div className={`${styles.alignable} ${styles.main}`}>
<div className={styles.main}>
{children}
</div>
</Panel>

View File

@ -1,39 +1,39 @@
.alignable {
margin: 0px auto;
.root {
margin: auto;
}
@media (max-width: 749px) and (min-width: 495px) {
.alignable {
.root {
width: 495px;
margin: auto;
}
}
@media (max-width: 1004px) and (min-width: 750px) {
.alignable {
.root {
width: 750px;
margin: auto;
}
}
@media (max-width: 1259px) and (min-width: 1005px) {
.alignable {
@media (max-width: 1514px) and (min-width: 1005px) {
.root {
width: 1005px;
margin: auto;
}
}
@media (max-width: 1514px) and (min-width: 1260px) {
.alignable {
width: 1260px;
}
}
@media (max-width: 1769px) and (min-width: 1515px) {
.alignable {
.root {
width: 1515px;
margin: auto;
}
}
@media (min-width: 1770px) {
.alignable {
.root {
width: 1770px;
margin: auto;
}
}

View File

@ -9,6 +9,13 @@ import styles from './CollectionPage.css';
import CollectionPageHOC from './editorialWorkflow/CollectionPageHOC';
class DashboardPage extends React.Component {
static propTypes = {
collection: ImmutablePropTypes.map.isRequired,
collections: ImmutablePropTypes.orderedMap.isRequired,
dispatch: PropTypes.func.isRequired,
entries: ImmutablePropTypes.list,
};
componentDidMount() {
const { collection, dispatch } = this.props;
if (collection) {
@ -30,7 +37,7 @@ class DashboardPage extends React.Component {
}
return <div className={styles.alignable}>
return <div className={styles.root}>
{entries ?
<EntryListing collection={collection} entries={entries}/>
:
@ -39,12 +46,6 @@ class DashboardPage extends React.Component {
</div>;
}
}
DashboardPage.propTypes = {
collection: ImmutablePropTypes.map.isRequired,
collections: ImmutablePropTypes.orderedMap.isRequired,
dispatch: PropTypes.func.isRequired,
entries: ImmutablePropTypes.list,
};
/*
* Instead of checking the publish mode everywhere to dispatch & render the additional editorial workflow stuff,

View File

@ -15,11 +15,22 @@ import EntryEditor from '../components/EntryEditor/EntryEditor';
import EntryPageHOC from './editorialWorkflow/EntryPageHOC';
class EntryPage extends React.Component {
constructor(props) {
super(props);
this.createDraft = this.createDraft.bind(this);
this.handlePersistEntry = this.handlePersistEntry.bind(this);
}
static propTypes = {
addMedia: PropTypes.func.isRequired,
boundGetMedia: PropTypes.func.isRequired,
changeDraft: PropTypes.func.isRequired,
collection: ImmutablePropTypes.map.isRequired,
createDraftFromEntry: PropTypes.func.isRequired,
createEmptyDraft: PropTypes.func.isRequired,
discardDraft: PropTypes.func.isRequired,
entry: ImmutablePropTypes.map,
entryDraft: ImmutablePropTypes.map.isRequired,
loadEntry: PropTypes.func.isRequired,
persistEntry: PropTypes.func.isRequired,
removeMedia: PropTypes.func.isRequired,
slug: PropTypes.string,
newEntry: PropTypes.bool.isRequired,
};
componentDidMount() {
if (!this.props.newEntry) {
@ -45,13 +56,13 @@ class EntryPage extends React.Component {
this.props.discardDraft();
}
createDraft(entry) {
createDraft = entry => {
if (entry) this.props.createDraftFromEntry(entry);
}
};
handlePersistEntry() {
handlePersistEntry = () => {
this.props.persistEntry(this.props.collection, this.props.entryDraft);
}
};
render() {
const {
@ -75,23 +86,6 @@ class EntryPage extends React.Component {
}
}
EntryPage.propTypes = {
addMedia: PropTypes.func.isRequired,
boundGetMedia: PropTypes.func.isRequired,
changeDraft: PropTypes.func.isRequired,
collection: ImmutablePropTypes.map.isRequired,
createDraftFromEntry: PropTypes.func.isRequired,
createEmptyDraft: PropTypes.func.isRequired,
discardDraft: PropTypes.func.isRequired,
entry: ImmutablePropTypes.map,
entryDraft: ImmutablePropTypes.map.isRequired,
loadEntry: PropTypes.func.isRequired,
persistEntry: PropTypes.func.isRequired,
removeMedia: PropTypes.func.isRequired,
slug: PropTypes.string,
newEntry: PropTypes.bool.isRequired,
};
function mapStateToProps(state, ownProps) {
const { collections, entryDraft } = state;
const collection = collections.get(ownProps.params.name);

View File

@ -10,6 +10,11 @@ import styles from '../CollectionPage.css';
export default function CollectionPageHOC(CollectionPage) {
class CollectionPageHOC extends CollectionPage {
static propTypes = {
dispatch: PropTypes.func.isRequired,
isEditorialWorkflow: PropTypes.bool.isRequired,
unpublishedEntries: ImmutablePropTypes.map,
};
componentDidMount() {
const { dispatch, isEditorialWorkflow } = this.props;
@ -24,24 +29,20 @@ export default function CollectionPageHOC(CollectionPage) {
if (!isEditorialWorkflow) return super.render();
return (
<div className={styles.alignable}>
<UnpublishedListing
<div>
<div className={styles.root}>
<UnpublishedListing
entries={unpublishedEntries}
handleChangeStatus={updateUnpublishedEntryStatus}
handlePublish={publishUnpublishedEntry}
/>
/>
</div>
{super.render()}
</div>
);
}
}
CollectionPageHOC.propTypes = {
dispatch: PropTypes.func.isRequired,
isEditorialWorkflow: PropTypes.bool.isRequired,
unpublishedEntries: ImmutablePropTypes.map,
};
function mapStateToProps(state) {
const publish_mode = state.config.get('publish_mode');
const isEditorialWorkflow = (publish_mode === EDITORIAL_WORKFLOW);