Merge pull request #376 from netlify/entry-editor-ui
Move to static rich text editor control bar
This commit is contained in:
commit
754369d80e
@ -141,7 +141,6 @@
|
|||||||
CMS.registerEditorComponent({
|
CMS.registerEditorComponent({
|
||||||
id: "youtube",
|
id: "youtube",
|
||||||
label: "Youtube",
|
label: "Youtube",
|
||||||
icon: 'video',
|
|
||||||
fields: [{name: 'id', label: 'Youtube Video ID'}],
|
fields: [{name: 'id', label: 'Youtube Video ID'}],
|
||||||
pattern: /^{{<\s?youtube (\S+)\s?>}}/,
|
pattern: /^{{<\s?youtube (\S+)\s?>}}/,
|
||||||
fromBlock: function(match) {
|
fromBlock: function(match) {
|
||||||
|
@ -94,6 +94,7 @@
|
|||||||
"@kadira/storybook": "^1.36.0",
|
"@kadira/storybook": "^1.36.0",
|
||||||
"autoprefixer": "^6.3.3",
|
"autoprefixer": "^6.3.3",
|
||||||
"babel-plugin-transform-builtin-extend": "^1.1.0",
|
"babel-plugin-transform-builtin-extend": "^1.1.0",
|
||||||
|
"classnames": "^2.2.5",
|
||||||
"dateformat": "^1.0.12",
|
"dateformat": "^1.0.12",
|
||||||
"deep-equal": "^1.0.1",
|
"deep-equal": "^1.0.1",
|
||||||
"fuzzy": "^0.1.1",
|
"fuzzy": "^0.1.1",
|
||||||
@ -146,6 +147,7 @@
|
|||||||
"react-simple-dnd": "^0.1.2",
|
"react-simple-dnd": "^0.1.2",
|
||||||
"react-sortable": "^1.2.0",
|
"react-sortable": "^1.2.0",
|
||||||
"react-split-pane": "^0.1.57",
|
"react-split-pane": "^0.1.57",
|
||||||
|
"react-textarea-autosize-inputref": "^4.1.0",
|
||||||
"react-toolbox": "^1.2.1",
|
"react-toolbox": "^1.2.1",
|
||||||
"react-topbar-progress-indicator": "^1.0.0",
|
"react-topbar-progress-indicator": "^1.0.0",
|
||||||
"react-waypoint": "^3.1.3",
|
"react-waypoint": "^3.1.3",
|
||||||
|
@ -7,9 +7,14 @@
|
|||||||
|
|
||||||
.previewToggle {
|
.previewToggle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 8px;
|
||||||
right: 40px;
|
right: 40px;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewToggleShow {
|
||||||
|
right: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controlPane {
|
.controlPane {
|
||||||
@ -17,7 +22,7 @@
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 20px 20px 0;
|
padding: 20px 20px 0;
|
||||||
border-right: 1px solid var(--defaultColorLight);
|
border-right: 1px solid var(--defaultColorLight);
|
||||||
background-color: color(#f2f5f4 lightness(90%));
|
background-color: var(--backgroundTertiaryColorDark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.previewPane {
|
.previewPane {
|
||||||
|
@ -2,11 +2,14 @@ import React, { Component, PropTypes } from 'react';
|
|||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import SplitPane from 'react-split-pane';
|
import SplitPane from 'react-split-pane';
|
||||||
import Button from 'react-toolbox/lib/button';
|
import Button from 'react-toolbox/lib/button';
|
||||||
|
import classnames from 'classnames';
|
||||||
import { ScrollSync, ScrollSyncPane } from '../ScrollSync';
|
import { ScrollSync, ScrollSyncPane } from '../ScrollSync';
|
||||||
import ControlPane from '../ControlPanel/ControlPane';
|
import ControlPane from '../ControlPanel/ControlPane';
|
||||||
import PreviewPane from '../PreviewPane/PreviewPane';
|
import PreviewPane from '../PreviewPane/PreviewPane';
|
||||||
import Toolbar from './EntryEditorToolbar';
|
import Toolbar from './EntryEditorToolbar';
|
||||||
|
import { StickyContext } from '../UI/Sticky/Sticky';
|
||||||
import styles from './EntryEditor.css';
|
import styles from './EntryEditor.css';
|
||||||
|
import stickyStyles from '../UI/Sticky/Sticky.css';
|
||||||
|
|
||||||
const PREVIEW_VISIBLE = 'cms.preview-visible';
|
const PREVIEW_VISIBLE = 'cms.preview-visible';
|
||||||
|
|
||||||
@ -50,17 +53,25 @@ class EntryEditor extends Component {
|
|||||||
onCancelEdit,
|
onCancelEdit,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const controlClassName = `${ styles.controlPane } ${ this.state.showEventBlocker && styles.blocker }`;
|
const { previewVisible, showEventBlocker } = this.state;
|
||||||
const previewClassName = `${ styles.previewPane } ${ this.state.showEventBlocker && styles.blocker }`;
|
|
||||||
|
|
||||||
const collectionPreviewEnabled = collection.getIn(['editor', 'preview'], true);
|
const collectionPreviewEnabled = collection.getIn(['editor', 'preview'], true);
|
||||||
|
|
||||||
const togglePreviewButton = (
|
const togglePreviewButton = (
|
||||||
<Button className={styles.previewToggle} onClick={this.handleTogglePreview}>Toggle Preview</Button>
|
<Button
|
||||||
|
className={classnames(styles.previewToggle, { previewVisible: styles.previewToggleShow })}
|
||||||
|
onClick={this.handleTogglePreview}
|
||||||
|
icon={previewVisible ? 'visibility_off' : 'visibility'}
|
||||||
|
floating
|
||||||
|
mini
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const editor = (
|
const editor = (
|
||||||
<div className={controlClassName}>
|
<StickyContext
|
||||||
|
className={classnames(styles.controlPane, { [styles.blocker]: showEventBlocker })}
|
||||||
|
registerListener={fn => this.updateStickyContext = fn}
|
||||||
|
>
|
||||||
{ collectionPreviewEnabled ? togglePreviewButton : null }
|
{ collectionPreviewEnabled ? togglePreviewButton : null }
|
||||||
<ControlPane
|
<ControlPane
|
||||||
collection={collection}
|
collection={collection}
|
||||||
@ -75,7 +86,7 @@ class EntryEditor extends Component {
|
|||||||
onRemoveAsset={onRemoveAsset}
|
onRemoveAsset={onRemoveAsset}
|
||||||
ref={c => this.controlPaneRef = c} // eslint-disable-line
|
ref={c => this.controlPaneRef = c} // eslint-disable-line
|
||||||
/>
|
/>
|
||||||
</div>
|
</StickyContext>
|
||||||
);
|
);
|
||||||
|
|
||||||
const editorWithPreview = (
|
const editorWithPreview = (
|
||||||
@ -85,9 +96,10 @@ class EntryEditor extends Component {
|
|||||||
defaultSize="50%"
|
defaultSize="50%"
|
||||||
onDragStarted={this.handleSplitPaneDragStart}
|
onDragStarted={this.handleSplitPaneDragStart}
|
||||||
onDragFinished={this.handleSplitPaneDragFinished}
|
onDragFinished={this.handleSplitPaneDragFinished}
|
||||||
|
onChange={this.updateStickyContext}
|
||||||
>
|
>
|
||||||
<ScrollSyncPane>{editor}</ScrollSyncPane>
|
<ScrollSyncPane>{editor}</ScrollSyncPane>
|
||||||
<div className={previewClassName}>
|
<div className={classnames(styles.previewPane, { [styles.blocker]: showEventBlocker })}>
|
||||||
<PreviewPane
|
<PreviewPane
|
||||||
collection={collection}
|
collection={collection}
|
||||||
entry={entry}
|
entry={entry}
|
||||||
|
19
src/components/UI/Sticky/Sticky.css
Normal file
19
src/components/UI/Sticky/Sticky.css
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
.stickyContainer {
|
||||||
|
position: relative !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sticky {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stickyActive:not(.stickyAtBottom) {
|
||||||
|
position: fixed !important;
|
||||||
|
top: 64px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stickyAtBottom {
|
||||||
|
position: absolute !important;
|
||||||
|
top: auto !important;
|
||||||
|
bottom: 30px !important;
|
||||||
|
}
|
||||||
|
|
219
src/components/UI/Sticky/Sticky.js
Normal file
219
src/components/UI/Sticky/Sticky.js
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { partial, without } from 'lodash';
|
||||||
|
import styles from './Sticky.css';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sticky is a collection of three components meant to facilitate "sticky" UI
|
||||||
|
* behavior for nested components. It uses React Context to provide an isolated,
|
||||||
|
* children-accessible state machine. It was specifically built for the rich
|
||||||
|
* text editor toolbar to achieve the following:
|
||||||
|
*
|
||||||
|
* - work within a scrollable section as if it were the window
|
||||||
|
* - remain at the top of the scrollable section if the rich text field begins
|
||||||
|
* to scroll up and out
|
||||||
|
* - scroll away with the rich text field when it is almost out of view
|
||||||
|
* - work when multiple rich text fields are present
|
||||||
|
*
|
||||||
|
* No available solution was near facilitating this for a React app. Eventually,
|
||||||
|
* if use continues, it should be improved to be more abstract and potentially
|
||||||
|
* split off to a separate library unto itself, covering more use cases than
|
||||||
|
* just the rich text toolbar.
|
||||||
|
*
|
||||||
|
* Sticky consists of three components, which are documented right here to
|
||||||
|
* facilitate a concise, high level overview:
|
||||||
|
*
|
||||||
|
* - StickyContext: the scrollable area that essentially serves as the window
|
||||||
|
* should be wrapped in StickyContext
|
||||||
|
* - StickyContainer: wraps the secondary container that the sticky element is
|
||||||
|
* bound within, eg. the rich text field
|
||||||
|
* - Sticky: wraps the sticky element itself
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class StickyContext extends Component {
|
||||||
|
static childContextTypes = {
|
||||||
|
subscribeToStickyContext: PropTypes.func,
|
||||||
|
unsubscribeToStickyContext: PropTypes.func,
|
||||||
|
requestUpdate: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* registerListener: accepts a function that is called with the `updateStickies` method as an
|
||||||
|
* arg, so it can be accessed from the component implementation site similar to how refs are
|
||||||
|
* accessed:
|
||||||
|
*
|
||||||
|
* <StickyContext registerListener={fn => this.updateStickyContext = fn}>
|
||||||
|
*
|
||||||
|
* This function can then be used from the component implementation site to force update the
|
||||||
|
* entire Sticky instance, which is sometimes necessary.
|
||||||
|
*/
|
||||||
|
registerListener: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.subscriptions = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeToStickyContext = (fn) => {
|
||||||
|
this.subscriptions.push(fn);
|
||||||
|
};
|
||||||
|
|
||||||
|
getChildContext() {
|
||||||
|
return {
|
||||||
|
subscribeToStickyContext: this.subscribeToStickyContext,
|
||||||
|
unsubscribeToStickyContext: (fn) => { this.subscriptions = without(this.subscriptions, fn); },
|
||||||
|
requestUpdate: () => { window.setTimeout(() => { this.updateStickies.call(this, this.ref); }); },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStickies = (ref) => {
|
||||||
|
const stickyContextTop = ref && ref.getBoundingClientRect().top;
|
||||||
|
this.subscriptions.forEach((fn) => { fn(stickyContextTop); });
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.updateStickies(this.ref);
|
||||||
|
this.props.registerListener(this.updateStickies.bind(this, this.ref));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleScroll = (event) => {
|
||||||
|
this.updateStickies(event.target);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className={this.props.className} onScroll={this.handleScroll} ref={(ref) => { this.ref = ref; }}>
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StickyContainer extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.subscriptions = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
subscribeToStickyContext: PropTypes.func,
|
||||||
|
unsubscribeToStickyContext: PropTypes.func,
|
||||||
|
requestUpdate: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
static childContextTypes = {
|
||||||
|
subscribeToStickyContainer: PropTypes.func,
|
||||||
|
unsubscribeToStickyContainer: PropTypes.func,
|
||||||
|
requestUpdate: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
getChildContext() {
|
||||||
|
return {
|
||||||
|
subscribeToStickyContainer: (fn) => { this.subscriptions.push(fn); },
|
||||||
|
unsubscribeToStickyContainer: (fn) => { this.subscriptions = without(this.subscriptions, fn); },
|
||||||
|
requestUpdate: () => { this.context.requestUpdate.call(this); },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getPosition: used for updating the sticky element to stick or not stick, and also provides
|
||||||
|
* width info. Because a sticky element uses fixed positioning, it may not be able to be sized
|
||||||
|
* relative to a parent, so the StickyContainer width is provided to allow the Sticky to be sized
|
||||||
|
* accordingly.
|
||||||
|
*/
|
||||||
|
getPosition = (contextTop) => {
|
||||||
|
const rect = this.ref.getBoundingClientRect();
|
||||||
|
const shouldStick = rect.top < contextTop;
|
||||||
|
const shouldStickAtBottom = rect.bottom - 60 < contextTop;
|
||||||
|
this.subscriptions.forEach((fn) => { fn(shouldStick, shouldStickAtBottom, rect.width); });
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.context.subscribeToStickyContext(this.getPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.context.unsubscribeToStickyContext(this.getPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id={this.context.string}
|
||||||
|
className={classnames(this.props.className, styles.stickyContainer)}
|
||||||
|
ref={(ref) => { this.ref = ref }}
|
||||||
|
>
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Sticky extends Component {
|
||||||
|
static contextTypes = {
|
||||||
|
subscribeToStickyContainer: PropTypes.func,
|
||||||
|
unsubscribeToStickyContainer: PropTypes.func,
|
||||||
|
requestUpdate: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* fillContainerWidth: allows the sticky width to be dynamically set to the width of it's
|
||||||
|
* StickyContainer when sticky (fixed positioning).
|
||||||
|
*/
|
||||||
|
fillContainerWidth: PropTypes.bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
this.state = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSticky = (shouldStick, shouldStickAtBottom, containerWidth) => {
|
||||||
|
this.setState({ shouldStick, shouldStickAtBottom, containerWidth });
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.context.subscribeToStickyContainer(this.updateSticky);
|
||||||
|
this.context.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.context.unsubscribeToStickyContainer(this.updateSticky);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { props, state } = this;
|
||||||
|
const stickyPlaceholderHeight = state.shouldStick ? this.ref.getBoundingClientRect().height : 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{paddingBottom: stickyPlaceholderHeight}}></div>
|
||||||
|
<div
|
||||||
|
className={classnames(
|
||||||
|
props.className,
|
||||||
|
styles.sticky,
|
||||||
|
{
|
||||||
|
[styles.stickyActive]: state.shouldStick,
|
||||||
|
[styles.stickyAtBottom]: state.shouldStickAtBottom,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
style={
|
||||||
|
props.fillContainerWidth && state.containerWidth && state.shouldStick ?
|
||||||
|
{ width: state.containerWidth } :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
ref={(ref) => {this.ref = ref}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -8,16 +8,24 @@
|
|||||||
--successColor: #1c7;
|
--successColor: #1c7;
|
||||||
--warningColor: #fa0;
|
--warningColor: #fa0;
|
||||||
--errorColor: #f52;
|
--errorColor: #f52;
|
||||||
|
--textColor: #272e30;
|
||||||
--borderRadius: 2px;
|
--borderRadius: 2px;
|
||||||
--borderRadiusLarge: 10px;
|
--borderRadiusLarge: 10px;
|
||||||
|
--dropShadow:
|
||||||
|
0 2px 2px 0 rgba(0, 0, 0, 0.14),
|
||||||
|
0 3px 1px -2px rgba(0, 0, 0, 0.2),
|
||||||
|
0 1px 5px 0 rgba(0, 0, 0, 0.12);
|
||||||
--topmostZindex: 99999;
|
--topmostZindex: 99999;
|
||||||
--foregroundAltColor: #fff;
|
--foregroundAltColor: #fff;
|
||||||
--backgroundAltColor: #272e30;
|
--backgroundAltColor: #272e30;
|
||||||
--textFieldBorderColor: #e7e7e7;
|
--textFieldBorderColor: #e7e7e7;
|
||||||
--highlightFGColor: #fff;
|
--highlightFGColor: #fff;
|
||||||
--highlightBGColor: #3ab7a5;
|
--highlightBGColor: #3ab7a5;
|
||||||
--controlLabelColor: #272e30;
|
--highlightFGAltColor: #eee;
|
||||||
|
--controlLabelColor: var(--textColor);
|
||||||
--controlBGColor: #fff;
|
--controlBGColor: #fff;
|
||||||
|
--backgroundTertiaryColor: #f2f5f4;
|
||||||
|
--backgroundTertiaryColorDark: color(var(--backgroundTertiaryColor) lightness(90%));
|
||||||
}
|
}
|
||||||
|
|
||||||
.base {
|
.base {
|
||||||
@ -36,3 +44,9 @@
|
|||||||
.depth {
|
.depth {
|
||||||
box-shadow: var(--shadowColor) 0 1px 6px;
|
box-shadow: var(--shadowColor) 0 1px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.clearfix:after {
|
||||||
|
content: '';
|
||||||
|
display: table;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
@ -3,18 +3,16 @@ import registry from '../../lib/registry';
|
|||||||
import RawEditor from './MarkdownControlElements/RawEditor';
|
import RawEditor from './MarkdownControlElements/RawEditor';
|
||||||
import VisualEditor from './MarkdownControlElements/VisualEditor';
|
import VisualEditor from './MarkdownControlElements/VisualEditor';
|
||||||
import { processEditorPlugins } from './richText';
|
import { processEditorPlugins } from './richText';
|
||||||
import { connect } from 'react-redux';
|
import { StickyContainer } from '../UI/Sticky/Sticky';
|
||||||
import { switchVisualMode } from '../../actions/editor';
|
|
||||||
|
|
||||||
const MODE_STORAGE_KEY = 'cms.md-mode';
|
const MODE_STORAGE_KEY = 'cms.md-mode';
|
||||||
|
|
||||||
class MarkdownControl extends React.Component {
|
export default class MarkdownControl extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
editor: PropTypes.object.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
onAddAsset: PropTypes.func.isRequired,
|
onAddAsset: PropTypes.func.isRequired,
|
||||||
|
onRemoveAsset: PropTypes.func.isRequired,
|
||||||
getAsset: PropTypes.func.isRequired,
|
getAsset: PropTypes.func.isRequired,
|
||||||
switchVisualMode: PropTypes.func.isRequired,
|
|
||||||
value: PropTypes.node,
|
value: PropTypes.node,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -35,8 +33,7 @@ class MarkdownControl extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const { onChange, onAddAsset, onRemoveAsset, getAsset, value } = this.props;
|
const { onChange, onAddAsset, onRemoveAsset, getAsset, value } = this.props;
|
||||||
const { mode } = this.state;
|
const { mode } = this.state;
|
||||||
if (mode === 'visual') {
|
const visualEditor = (
|
||||||
return (
|
|
||||||
<div className="cms-editor-visual">
|
<div className="cms-editor-visual">
|
||||||
<VisualEditor
|
<VisualEditor
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
@ -48,9 +45,7 @@ class MarkdownControl extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
const rawEditor = (
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="cms-editor-raw">
|
<div className="cms-editor-raw">
|
||||||
<RawEditor
|
<RawEditor
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
@ -62,10 +57,6 @@ class MarkdownControl extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
return <StickyContainer>{ mode === 'visual' ? visualEditor : rawEditor }</StickyContainer>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
|
||||||
state => ({ editor: state.editor }),
|
|
||||||
{ switchVisualMode }
|
|
||||||
)(MarkdownControl);
|
|
||||||
|
@ -1,135 +0,0 @@
|
|||||||
.root {
|
|
||||||
position: absolute;
|
|
||||||
left: -18px;
|
|
||||||
display: none;
|
|
||||||
width: 100%;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.visible {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
display: block;
|
|
||||||
width: 15px;
|
|
||||||
height: 15px;
|
|
||||||
border: 1px solid #444;
|
|
||||||
border-radius: 100%;
|
|
||||||
background: transparent;
|
|
||||||
line-height: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.expanded {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapsed {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pluginForm,
|
|
||||||
.menu {
|
|
||||||
margin-top: -20px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
margin-left: 20px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 1px 1px 20px;
|
|
||||||
|
|
||||||
& h3 {
|
|
||||||
padding: 5px 20px;
|
|
||||||
border-bottom: 1px solid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu {
|
|
||||||
list-style: none;
|
|
||||||
|
|
||||||
& li button {
|
|
||||||
display: block;
|
|
||||||
padding: 5px 20px;
|
|
||||||
min-width: 30%;
|
|
||||||
width: 100%;
|
|
||||||
border: none;
|
|
||||||
border-bottom: 1px solid;
|
|
||||||
background: #fff;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
& li:last-child button {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
& li:hover button {
|
|
||||||
background: #efefef;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu.expanded {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control {
|
|
||||||
position: relative;
|
|
||||||
padding: 20px 20px;
|
|
||||||
color: #7c8382;
|
|
||||||
|
|
||||||
& input,
|
|
||||||
& textarea,
|
|
||||||
& select {
|
|
||||||
display: block;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
width: 100%;
|
|
||||||
outline: 0;
|
|
||||||
border: none;
|
|
||||||
background: 0 0;
|
|
||||||
box-shadow: none;
|
|
||||||
color: #7c8382;
|
|
||||||
font-size: 18px;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 18px;
|
|
||||||
color: #aab0af;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.widget {
|
|
||||||
position: relative;
|
|
||||||
border-bottom: 1px solid #aaa;
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
position: absolute;
|
|
||||||
bottom: -7px;
|
|
||||||
left: 42px;
|
|
||||||
z-index: 1;
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-right: 1px solid #aaa;
|
|
||||||
border-bottom: 1px solid #aaa;
|
|
||||||
background-color: #f2f5f4;
|
|
||||||
content: '';
|
|
||||||
-webkit-transform: rotate(45deg);
|
|
||||||
transform: rotate(45deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child:after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-top: 1px solid #eee;
|
|
||||||
background: #fff;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
@ -1,134 +0,0 @@
|
|||||||
import React, { Component, PropTypes } from 'react';
|
|
||||||
import { fromJS } from 'immutable';
|
|
||||||
import { Button } from 'react-toolbox/lib/button';
|
|
||||||
import { resolveWidget } from '../../Widgets';
|
|
||||||
import styles from './BlockMenu.css';
|
|
||||||
|
|
||||||
export default class BlockMenu extends Component {
|
|
||||||
static propTypes = {
|
|
||||||
isOpen: PropTypes.bool,
|
|
||||||
selectionPosition: PropTypes.object,
|
|
||||||
plugins: PropTypes.object.isRequired,
|
|
||||||
onBlock: PropTypes.func.isRequired,
|
|
||||||
onAddAsset: PropTypes.func.isRequired,
|
|
||||||
onRemoveAsset: PropTypes.func.isRequired,
|
|
||||||
getAsset: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
isExpanded: false,
|
|
||||||
openPlugin: null,
|
|
||||||
pluginData: fromJS({}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
const { selectionPosition } = this.props;
|
|
||||||
if (selectionPosition) {
|
|
||||||
const style = this.element.style;
|
|
||||||
style.setProperty('top', `${ selectionPosition.top }px`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleToggle = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
this.setState({ isExpanded: !this.state.isExpanded });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleRef = (ref) => {
|
|
||||||
this.element = ref;
|
|
||||||
};
|
|
||||||
|
|
||||||
handlePlugin(plugin) {
|
|
||||||
return (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
this.setState({ openPlugin: plugin, pluginData: fromJS({}) });
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
buttonFor(plugin) {
|
|
||||||
return (<li key={`plugin-${ plugin.get('id') }`}>
|
|
||||||
<button onClick={this.handlePlugin(plugin)}>{plugin.get('label')}</button>
|
|
||||||
</li>);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSubmit = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const { openPlugin, pluginData } = this.state;
|
|
||||||
this.props.onBlock(openPlugin, pluginData);
|
|
||||||
this.setState({ openPlugin: null, isExpanded: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleCancel = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
this.setState({ openPlugin: null, isExpanded: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
controlFor(field) {
|
|
||||||
const { onAddAsset, onRemoveAsset, getAsset } = this.props;
|
|
||||||
const { pluginData } = this.state;
|
|
||||||
const widget = resolveWidget(field.get('widget') || 'string');
|
|
||||||
const value = pluginData.get(field.get('name'));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.control} key={`field-${ field.get('name') }`}>
|
|
||||||
<label className={styles.label}>{field.get('label')}</label>
|
|
||||||
{
|
|
||||||
React.createElement(widget.control, {
|
|
||||||
field,
|
|
||||||
value,
|
|
||||||
onChange: (val) => {
|
|
||||||
this.setState({
|
|
||||||
pluginData: pluginData.set(field.get('name'), val),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onAddAsset,
|
|
||||||
onRemoveAsset,
|
|
||||||
getAsset,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pluginForm(plugin) {
|
|
||||||
return (<form className={styles.pluginForm} onSubmit={this.handleSubmit}>
|
|
||||||
<h3>Insert {plugin.get('label')}</h3>
|
|
||||||
{plugin.get('fields').map(field => this.controlFor(field))}
|
|
||||||
<div className={styles.footer}>
|
|
||||||
<Button
|
|
||||||
raised
|
|
||||||
onClick={this.handleSubmit}
|
|
||||||
>
|
|
||||||
Insert
|
|
||||||
</Button>
|
|
||||||
{' '}
|
|
||||||
<Button onClick={this.handleCancel}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { isOpen, plugins } = this.props;
|
|
||||||
const { isExpanded, openPlugin } = this.state;
|
|
||||||
const classNames = [styles.root];
|
|
||||||
if (isOpen) {
|
|
||||||
classNames.push(styles.visible);
|
|
||||||
}
|
|
||||||
if (openPlugin) {
|
|
||||||
classNames.push(styles.openPlugin);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (<div className={classNames.join(' ')} ref={this.handleRef}>
|
|
||||||
<button className={styles.button} onClick={this.handleToggle}>+</button>
|
|
||||||
<ul className={[styles.menu, isExpanded && !openPlugin ? styles.expanded : styles.collapsed].join(' ')}>
|
|
||||||
{plugins.map(plugin => this.buttonFor(plugin))}
|
|
||||||
</ul>
|
|
||||||
{openPlugin && this.pluginForm(openPlugin)}
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,6 +2,10 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editorControlBar {
|
||||||
|
composes: editorControlBar from "../VisualEditor/index.css";
|
||||||
|
}
|
||||||
|
|
||||||
.dragging { }
|
.dragging { }
|
||||||
|
|
||||||
.shim {
|
.shim {
|
||||||
|
@ -3,10 +3,11 @@ import MarkupIt from 'markup-it';
|
|||||||
import markdownSyntax from 'markup-it/syntaxes/markdown';
|
import markdownSyntax from 'markup-it/syntaxes/markdown';
|
||||||
import htmlSyntax from 'markup-it/syntaxes/html';
|
import htmlSyntax from 'markup-it/syntaxes/html';
|
||||||
import CaretPosition from 'textarea-caret-position';
|
import CaretPosition from 'textarea-caret-position';
|
||||||
|
import TextareaAutosize from 'react-textarea-autosize-inputref';
|
||||||
import registry from '../../../../lib/registry';
|
import registry from '../../../../lib/registry';
|
||||||
import { createAssetProxy } from '../../../../valueObjects/AssetProxy';
|
import { createAssetProxy } from '../../../../valueObjects/AssetProxy';
|
||||||
import Toolbar from '../Toolbar';
|
import Toolbar from '../Toolbar/Toolbar';
|
||||||
import BlockMenu from '../BlockMenu';
|
import { Sticky } from '../../../UI/Sticky/Sticky';
|
||||||
import styles from './index.css';
|
import styles from './index.css';
|
||||||
|
|
||||||
const HAS_LINE_BREAK = /\n/m;
|
const HAS_LINE_BREAK = /\n/m;
|
||||||
@ -18,7 +19,7 @@ function processUrl(url) {
|
|||||||
if (url.match(/^(https?:\/\/|mailto:|\/)/)) {
|
if (url.match(/^(https?:\/\/|mailto:|\/)/)) {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
if (url.match(/^[^\/]+\.[^\/]+/)) {
|
if (url.match(/^[^/]+\.[^/]+/)) {
|
||||||
return `https://${ url }`;
|
return `https://${ url }`;
|
||||||
}
|
}
|
||||||
return `/${ url }`;
|
return `/${ url }`;
|
||||||
@ -44,7 +45,9 @@ function getCleanPaste(e) {
|
|||||||
// Handle complex pastes by stealing focus with a contenteditable div
|
// Handle complex pastes by stealing focus with a contenteditable div
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.contentEditable = true;
|
div.contentEditable = true;
|
||||||
div.setAttribute('style', 'opacity: 0; overflow: hidden; width: 1px; height: 1px; position: fixed; top: 50%; left: 0;');
|
div.setAttribute(
|
||||||
|
'style', 'opacity: 0; overflow: hidden; width: 1px; height: 1px; position: fixed; top: 50%; left: 0;'
|
||||||
|
);
|
||||||
document.body.appendChild(div);
|
document.body.appendChild(div);
|
||||||
div.focus();
|
div.focus();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -194,7 +197,7 @@ export default class RawEditor extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleLink = () => {
|
handleLink = () => {
|
||||||
const url = prompt('URL:');
|
const url = prompt('URL:'); // eslint-disable-line no-alert
|
||||||
const selection = this.getSelection();
|
const selection = this.getSelection();
|
||||||
this.replaceSelection(`[${ selection.selected }](${ processUrl(url) })`);
|
this.replaceSelection(`[${ selection.selected }](${ processUrl(url) })`);
|
||||||
};
|
};
|
||||||
@ -205,9 +208,9 @@ export default class RawEditor extends React.Component {
|
|||||||
if (selection.start !== selection.end && !HAS_LINE_BREAK.test(selection.selected)) {
|
if (selection.start !== selection.end && !HAS_LINE_BREAK.test(selection.selected)) {
|
||||||
try {
|
try {
|
||||||
const selectionPosition = this.caretPosition.get(selection.start, selection.end);
|
const selectionPosition = this.caretPosition.get(selection.start, selection.end);
|
||||||
this.setState({ showToolbar: true, showBlockMenu: false, selectionPosition });
|
this.setState({ selectionPosition });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.setState({ showToolbar: false, showBlockMenu: false });
|
console.log(e); // eslint-disable-line no-console
|
||||||
}
|
}
|
||||||
} else if (selection.start === selection.end) {
|
} else if (selection.start === selection.end) {
|
||||||
const newBlock =
|
const newBlock =
|
||||||
@ -222,12 +225,8 @@ export default class RawEditor extends React.Component {
|
|||||||
|
|
||||||
if (newBlock) {
|
if (newBlock) {
|
||||||
const position = this.caretPosition.get(selection.start, selection.end);
|
const position = this.caretPosition.get(selection.start, selection.end);
|
||||||
this.setState({ showToolbar: false, showBlockMenu: true, selectionPosition: position });
|
this.setState({ selectionPosition: position });
|
||||||
} else {
|
|
||||||
this.setState({ showToolbar: false, showBlockMenu: false });
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
this.setState({ showToolbar: false, showBlockMenu: false });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -236,10 +235,9 @@ export default class RawEditor extends React.Component {
|
|||||||
this.updateHeight();
|
this.updateHeight();
|
||||||
};
|
};
|
||||||
|
|
||||||
handleBlock = (plugin, data) => {
|
handlePluginSubmit = (plugin, data) => {
|
||||||
const toBlock = plugin.get('toBlock');
|
const toBlock = plugin.get('toBlock');
|
||||||
this.replaceSelection(toBlock.call(toBlock, data.toJS()));
|
this.replaceSelection(toBlock.call(toBlock, data.toJS()));
|
||||||
this.setState({ showBlockMenu: false });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleHeader(header) {
|
handleHeader(header) {
|
||||||
@ -309,7 +307,7 @@ export default class RawEditor extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { onAddAsset, onRemoveAsset, getAsset } = this.props;
|
const { onAddAsset, onRemoveAsset, getAsset } = this.props;
|
||||||
const { showToolbar, showBlockMenu, plugins, selectionPosition, dragging } = this.state;
|
const { plugins, selectionPosition, dragging } = this.state;
|
||||||
const classNames = [styles.root];
|
const classNames = [styles.root];
|
||||||
if (dragging) {
|
if (dragging) {
|
||||||
classNames.push(styles.dragging);
|
classNames.push(styles.dragging);
|
||||||
@ -322,8 +320,8 @@ export default class RawEditor extends React.Component {
|
|||||||
onDragOver={this.handleDragOver}
|
onDragOver={this.handleDragOver}
|
||||||
onDrop={this.handleDrop}
|
onDrop={this.handleDrop}
|
||||||
>
|
>
|
||||||
|
<Sticky className={styles.editorControlBar} fillContainerWidth>
|
||||||
<Toolbar
|
<Toolbar
|
||||||
isOpen={showToolbar}
|
|
||||||
selectionPosition={selectionPosition}
|
selectionPosition={selectionPosition}
|
||||||
onH1={this.handleHeader('#')}
|
onH1={this.handleHeader('#')}
|
||||||
onH2={this.handleHeader('##')}
|
onH2={this.handleHeader('##')}
|
||||||
@ -331,18 +329,16 @@ export default class RawEditor extends React.Component {
|
|||||||
onItalic={this.handleItalic}
|
onItalic={this.handleItalic}
|
||||||
onLink={this.handleLink}
|
onLink={this.handleLink}
|
||||||
onToggleMode={this.handleToggle}
|
onToggleMode={this.handleToggle}
|
||||||
/>
|
|
||||||
<BlockMenu
|
|
||||||
isOpen={showBlockMenu}
|
|
||||||
selectionPosition={selectionPosition}
|
|
||||||
plugins={plugins}
|
plugins={plugins}
|
||||||
onBlock={this.handleBlock}
|
onSubmit={this.handlePluginSubmit}
|
||||||
onAddAsset={onAddAsset}
|
onAddAsset={onAddAsset}
|
||||||
onRemoveAsset={onRemoveAsset}
|
onRemoveAsset={onRemoveAsset}
|
||||||
getAsset={getAsset}
|
getAsset={getAsset}
|
||||||
|
rawMode
|
||||||
/>
|
/>
|
||||||
<textarea
|
</Sticky>
|
||||||
ref={this.handleRef}
|
<TextareaAutosize
|
||||||
|
inputRef={this.handleRef}
|
||||||
value={this.props.value || ''}
|
value={this.props.value || ''}
|
||||||
onKeyDown={this.handleKey}
|
onKeyDown={this.handleKey}
|
||||||
onChange={this.handleChange}
|
onChange={this.handleChange}
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
.Toolbar {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1000;
|
|
||||||
display: none;
|
|
||||||
margin: none;
|
|
||||||
padding: none;
|
|
||||||
box-shadow: 1px 1px 5px;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Button {
|
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
& button {
|
|
||||||
padding: 5px;
|
|
||||||
border: none;
|
|
||||||
border-right: 1px solid #eee;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.Button:last-child button {
|
|
||||||
border-right: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Visible {
|
|
||||||
display: block;
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
import React, { Component, PropTypes } from 'react';
|
|
||||||
import { Icon } from '../../UI';
|
|
||||||
import styles from './Toolbar.css';
|
|
||||||
|
|
||||||
function button(label, icon, action) {
|
|
||||||
return (<li className={styles.Button}>
|
|
||||||
<button className={styles[label]} onClick={action} title={label}>
|
|
||||||
<Icon type={icon} />
|
|
||||||
</button>
|
|
||||||
</li>);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Toolbar extends Component {
|
|
||||||
static propTypes = {
|
|
||||||
isOpen: PropTypes.bool,
|
|
||||||
selectionPosition: PropTypes.object,
|
|
||||||
onH1: PropTypes.func.isRequired,
|
|
||||||
onH2: PropTypes.func.isRequired,
|
|
||||||
onBold: PropTypes.func.isRequired,
|
|
||||||
onItalic: PropTypes.func.isRequired,
|
|
||||||
onLink: PropTypes.func.isRequired,
|
|
||||||
onToggleMode: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
const { selectionPosition } = this.props;
|
|
||||||
if (selectionPosition) {
|
|
||||||
const rect = this.element.getBoundingClientRect();
|
|
||||||
const parentRect = this.element.parentElement.getBoundingClientRect();
|
|
||||||
const style = this.element.style;
|
|
||||||
const pos = {
|
|
||||||
top: selectionPosition.top - rect.height - 5,
|
|
||||||
left: Math.min(selectionPosition.left, parentRect.width - rect.width),
|
|
||||||
};
|
|
||||||
style.setProperty('top', `${ pos.top }px`);
|
|
||||||
style.setProperty('left', `${ pos.left }px`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleRef = (ref) => {
|
|
||||||
this.element = ref;
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { isOpen, onH1, onH2, onBold, onItalic, onLink, onToggleMode } = this.props;
|
|
||||||
const classNames = [styles.Toolbar];
|
|
||||||
|
|
||||||
if (isOpen) {
|
|
||||||
classNames.push(styles.Visible);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ul className={classNames.join(' ')} ref={this.handleRef}>
|
|
||||||
{button('Header 1', 'h1', onH1)}
|
|
||||||
{button('Header 2', 'h2', onH2)}
|
|
||||||
{button('Bold', 'bold', onBold)}
|
|
||||||
{button('Italic', 'italic', onItalic)}
|
|
||||||
{button('Link', 'link', onLink)}
|
|
||||||
{button('View Code', 'code', onToggleMode)}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,13 @@
|
|||||||
|
@import "../../../UI/theme";
|
||||||
|
|
||||||
|
.Toolbar {
|
||||||
|
composes: clearfix;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Toggle {
|
||||||
|
float: right;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-top: 4px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
@ -0,0 +1,90 @@
|
|||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
import { List } from 'immutable';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import Switch from 'react-toolbox/lib/switch';
|
||||||
|
import ToolbarButton from './ToolbarButton';
|
||||||
|
import ToolbarComponentsMenu from './ToolbarComponentsMenu';
|
||||||
|
import ToolbarPluginForm from './ToolbarPluginForm';
|
||||||
|
import { Icon } from '../../../UI';
|
||||||
|
import styles from './Toolbar.css';
|
||||||
|
|
||||||
|
export default class Toolbar extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
selectionPosition: PropTypes.object,
|
||||||
|
onH1: PropTypes.func.isRequired,
|
||||||
|
onH2: PropTypes.func.isRequired,
|
||||||
|
onBold: PropTypes.func.isRequired,
|
||||||
|
onItalic: PropTypes.func.isRequired,
|
||||||
|
onLink: PropTypes.func.isRequired,
|
||||||
|
onToggleMode: PropTypes.func.isRequired,
|
||||||
|
rawMode: PropTypes.bool,
|
||||||
|
plugins: ImmutablePropTypes.listOf(ImmutablePropTypes.record),
|
||||||
|
onSubmit: PropTypes.func.isRequired,
|
||||||
|
onAddAsset: PropTypes.func.isRequired,
|
||||||
|
onRemoveAsset: PropTypes.func.isRequired,
|
||||||
|
getAsset: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
activePlugin: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePluginFormDisplay = (plugin) => {
|
||||||
|
this.setState({ activePlugin: plugin });
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePluginFormSubmit = (plugin, pluginData) => {
|
||||||
|
this.props.onSubmit(plugin, pluginData);
|
||||||
|
this.setState({ activePlugin: null });
|
||||||
|
};
|
||||||
|
|
||||||
|
handlePluginFormCancel = (e) => {
|
||||||
|
this.setState({ activePlugin: null });
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
onH1,
|
||||||
|
onH2,
|
||||||
|
onBold,
|
||||||
|
onItalic,
|
||||||
|
onLink,
|
||||||
|
onToggleMode,
|
||||||
|
rawMode,
|
||||||
|
plugins,
|
||||||
|
onAddAsset,
|
||||||
|
onRemoveAsset,
|
||||||
|
getAsset,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const { activePlugin } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.Toolbar}>
|
||||||
|
<ToolbarButton label="Header 1" icon="h1" action={onH1}/>
|
||||||
|
<ToolbarButton label="Header 2" icon="h2" action={onH2}/>
|
||||||
|
<ToolbarButton label="Bold" icon="bold" action={onBold}/>
|
||||||
|
<ToolbarButton label="Italic" icon="italic" action={onItalic}/>
|
||||||
|
<ToolbarButton label="Link" icon="link" action={onLink}/>
|
||||||
|
<ToolbarComponentsMenu
|
||||||
|
plugins={plugins}
|
||||||
|
onComponentMenuItemClick={this.handlePluginFormDisplay}
|
||||||
|
/>
|
||||||
|
{activePlugin &&
|
||||||
|
<ToolbarPluginForm
|
||||||
|
plugin={activePlugin}
|
||||||
|
onSubmit={this.handlePluginFormSubmit}
|
||||||
|
onCancel={this.handlePluginFormCancel}
|
||||||
|
onAddAsset={onAddAsset}
|
||||||
|
onRemoveAsset={onRemoveAsset}
|
||||||
|
getAsset={getAsset}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
<Switch label="Markdown" onChange={onToggleMode} checked={rawMode} className={styles.Toggle}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
@import "../../../UI/theme";
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 6px;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
background-color: var(--highlightFGAltColor);
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { Icon } from '../../../UI';
|
||||||
|
import styles from './ToolbarButton.css';
|
||||||
|
|
||||||
|
const ToolbarButton = ({ label, icon, action, active }) => (
|
||||||
|
<button
|
||||||
|
className={classnames(styles.button, { [styles.active]: active })}
|
||||||
|
onClick={action}
|
||||||
|
title={label}
|
||||||
|
>
|
||||||
|
{ icon ? <Icon type={icon} /> : label }
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
ToolbarButton.propTypes = {
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
icon: PropTypes.string,
|
||||||
|
action: PropTypes.func.isRequired,
|
||||||
|
active: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ToolbarButton;
|
@ -0,0 +1,14 @@
|
|||||||
|
.root {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuItem {
|
||||||
|
height: auto;
|
||||||
|
padding-top: 6px;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
|
||||||
|
& span {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { Menu, MenuItem } from 'react-toolbox/lib/menu';
|
||||||
|
import ToolbarButton from './ToolbarButton';
|
||||||
|
import styles from './ToolbarComponentsMenu.css';
|
||||||
|
|
||||||
|
export default class ToolbarComponentsMenu extends React.Component {
|
||||||
|
static PropTypes = {
|
||||||
|
plugins: ImmutablePropTypes.list.isRequired,
|
||||||
|
onComponentMenuItemClick: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
componentsMenuActive: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleComponentsMenuToggle = () => {
|
||||||
|
this.setState({ componentsMenuActive: !this.state.componentsMenuActive });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleComponentsMenuHide = () => {
|
||||||
|
this.setState({ componentsMenuActive: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { plugins, onComponentMenuItemClick } = this.props;
|
||||||
|
return (
|
||||||
|
<div className={styles.root}>
|
||||||
|
<ToolbarButton label="Add Component" icon="plus" action={this.handleComponentsMenuToggle}/>
|
||||||
|
<Menu
|
||||||
|
active={this.state.componentsMenuActive}
|
||||||
|
position="auto"
|
||||||
|
onHide={this.handleComponentsMenuHide}
|
||||||
|
ripple={false}
|
||||||
|
>
|
||||||
|
{plugins.map(plugin => (
|
||||||
|
<MenuItem
|
||||||
|
key={plugin.get('id')}
|
||||||
|
value={plugin.get('id')}
|
||||||
|
caption={plugin.get('label')}
|
||||||
|
onClick={() => onComponentMenuItemClick(plugin)}
|
||||||
|
className={styles.menuItem}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
@import "../../../UI/theme";
|
||||||
|
|
||||||
|
.pluginForm {
|
||||||
|
position: absolute;
|
||||||
|
background-color: var(--backgroundTertiaryColorDark);
|
||||||
|
margin-top: -20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
margin-left: 20px;
|
||||||
|
border-radius: var(--borderRadius);
|
||||||
|
box-shadow: var(--dropShadow);
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
/* Nested to override high specificity React Toolbox styles */
|
||||||
|
& .header {
|
||||||
|
padding: 12px;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--textColor);
|
||||||
|
background-color: var(--backgroundColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
padding: 0 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
background-color: var(--backgroundColor);
|
||||||
|
padding: 10px 20px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
import { Map } from 'immutable';
|
||||||
|
import { Button } from 'react-toolbox/lib/button';
|
||||||
|
import ToolbarPluginFormControl from './ToolbarPluginFormControl';
|
||||||
|
import styles from './ToolbarPluginForm.css';
|
||||||
|
|
||||||
|
export default class ToolbarPluginForm extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
plugin: PropTypes.object.isRequired,
|
||||||
|
onSubmit: PropTypes.func.isRequired,
|
||||||
|
onCancel: PropTypes.func.isRequired,
|
||||||
|
onAddAsset: PropTypes.func.isRequired,
|
||||||
|
onRemoveAsset: PropTypes.func.isRequired,
|
||||||
|
getAsset: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
data: Map(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit = (event) => {
|
||||||
|
const { plugin, onSubmit } = this.props;
|
||||||
|
onSubmit(plugin, this.state.data);
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
plugin,
|
||||||
|
onSubmit,
|
||||||
|
onCancel,
|
||||||
|
value,
|
||||||
|
onAddAsset,
|
||||||
|
onRemoveAsset,
|
||||||
|
getAsset,
|
||||||
|
onChange,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className={styles.pluginForm} onSubmit={this.handleSubmit}>
|
||||||
|
<h3 className={styles.header}>Insert {plugin.get('label')}</h3>
|
||||||
|
<div className={styles.body}>
|
||||||
|
{plugin.get('fields').map((field, index) => (
|
||||||
|
<ToolbarPluginFormControl
|
||||||
|
key={index}
|
||||||
|
field={field}
|
||||||
|
value={this.state.data.get(field.get('name'))}
|
||||||
|
onAddAsset={onAddAsset}
|
||||||
|
onRemoveAsset={onRemoveAsset}
|
||||||
|
getAsset={getAsset}
|
||||||
|
onChange={(val) => {
|
||||||
|
this.setState({ data: this.state.data.set(field.get('name'), val) });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className={styles.footer}>
|
||||||
|
<Button raised onClick={this.handleSubmit}>Insert</Button>
|
||||||
|
{' '}
|
||||||
|
<Button onClick={onCancel}>Cancel</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
.control {
|
||||||
|
composes: control from "../../../ControlPanel/ControlPane.css"
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
composes: label from "../../../ControlPanel/ControlPane.css";
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
import { resolveWidget } from '../../../Widgets';
|
||||||
|
import styles from './ToolbarPluginFormControl.css';
|
||||||
|
|
||||||
|
const ToolbarPluginFormControl = ({
|
||||||
|
field,
|
||||||
|
value,
|
||||||
|
pluginData,
|
||||||
|
onAddAsset,
|
||||||
|
onRemoveAsset,
|
||||||
|
getAsset,
|
||||||
|
onChange,
|
||||||
|
}) => {
|
||||||
|
const widget = resolveWidget(field.get('widget') || 'string');
|
||||||
|
const key = `field-${ field.get('name') }`;
|
||||||
|
const Control = widget.control;
|
||||||
|
const controlProps = { field, value, onAddAsset, onRemoveAsset, getAsset, onChange };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.control} key={key}>
|
||||||
|
<label className={styles.label} htmlFor={key}>{field.get('label')}</label>
|
||||||
|
<Control {...controlProps}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ToolbarPluginFormControl.propTypes = {
|
||||||
|
field: PropTypes.object.isRequired,
|
||||||
|
value: PropTypes.oneOfType([
|
||||||
|
PropTypes.string,
|
||||||
|
PropTypes.number,
|
||||||
|
]),
|
||||||
|
onAddAsset: PropTypes.func.isRequired,
|
||||||
|
onRemoveAsset: PropTypes.func.isRequired,
|
||||||
|
getAsset: PropTypes.func.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ToolbarPluginFormControl;
|
@ -1,5 +1,12 @@
|
|||||||
@import "../../../UI/theme";
|
@import "../../../UI/theme";
|
||||||
|
|
||||||
|
.editorControlBar {
|
||||||
|
background-color: var(--controlBGColor);
|
||||||
|
border-bottom: 1px solid var(--backgroundTertiaryColorDark);
|
||||||
|
border-radius: var(--borderRadius) var(--borderRadius) 0 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.editor {
|
.editor {
|
||||||
position: relative;
|
position: relative;
|
||||||
& h1, & h2, & h3 {
|
& h1, & h2, & h3 {
|
||||||
@ -61,7 +68,11 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
background-color: var(--controlBGColor);
|
background-color: var(--controlBGColor);
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-radius: var(--borderRadius);
|
border-radius: 0 0 var(--borderRadius) var(--borderRadius);
|
||||||
|
|
||||||
|
& ul {
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& .ProseMirror-content {
|
& .ProseMirror-content {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { Map } from 'immutable';
|
||||||
import { Schema } from 'prosemirror-model';
|
import { Schema } from 'prosemirror-model';
|
||||||
import { EditorState } from 'prosemirror-state';
|
import { EditorState } from 'prosemirror-state';
|
||||||
import { EditorView } from 'prosemirror-view';
|
import { EditorView } from 'prosemirror-view';
|
||||||
@ -8,21 +9,21 @@ import {
|
|||||||
inputRules, allInputRules,
|
inputRules, allInputRules,
|
||||||
} from 'prosemirror-inputrules';
|
} from 'prosemirror-inputrules';
|
||||||
import { keymap } from 'prosemirror-keymap';
|
import { keymap } from 'prosemirror-keymap';
|
||||||
import { schema, defaultMarkdownSerializer } from 'prosemirror-markdown';
|
import { schema as markdownSchema, defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||||
import { baseKeymap, setBlockType, toggleMark } from 'prosemirror-commands';
|
import { baseKeymap, setBlockType, toggleMark } from 'prosemirror-commands';
|
||||||
import registry from '../../../../lib/registry';
|
import registry from '../../../../lib/registry';
|
||||||
import { createAssetProxy } from '../../../../valueObjects/AssetProxy';
|
import { createAssetProxy } from '../../../../valueObjects/AssetProxy';
|
||||||
import { buildKeymap } from './keymap';
|
import { buildKeymap } from './keymap';
|
||||||
import createMarkdownParser from './parser';
|
import createMarkdownParser from './parser';
|
||||||
import Toolbar from '../Toolbar';
|
import Toolbar from '../Toolbar/Toolbar';
|
||||||
import BlockMenu from '../BlockMenu';
|
import { Sticky } from '../../../UI/Sticky/Sticky';
|
||||||
import styles from './index.css';
|
import styles from './index.css';
|
||||||
|
|
||||||
function processUrl(url) {
|
function processUrl(url) {
|
||||||
if (url.match(/^(https?:\/\/|mailto:|\/)/)) {
|
if (url.match(/^(https?:\/\/|mailto:|\/)/)) {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
if (url.match(/^[^\/]+\.[^\/]+/)) {
|
if (url.match(/^[^/]+\.[^/]+/)) {
|
||||||
return `https://${ url }`;
|
return `https://${ url }`;
|
||||||
}
|
}
|
||||||
return `/${ url }`;
|
return `/${ url }`;
|
||||||
@ -37,15 +38,10 @@ const ruleset = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function buildInputRules(schema) {
|
function buildInputRules(schema) {
|
||||||
const result = [];
|
return Map(ruleset)
|
||||||
for (const rule in ruleset) {
|
.filter(rule => schema.nodes[rule])
|
||||||
const type = schema.nodes[rule];
|
.map(rule => rule[0].apply(rule[0].slice(1)))
|
||||||
if (type) {
|
.toArray();
|
||||||
const fn = ruleset[rule];
|
|
||||||
result.push(fn[0].apply(fn.slice(1)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function markActive(state, type) {
|
function markActive(state, type) {
|
||||||
@ -99,12 +95,12 @@ export default class Editor extends Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
const plugins = registry.getEditorComponents();
|
const plugins = registry.getEditorComponents();
|
||||||
const s = schemaWithPlugins(schema, plugins);
|
const schema = schemaWithPlugins(markdownSchema, plugins);
|
||||||
this.state = {
|
this.state = {
|
||||||
plugins,
|
plugins,
|
||||||
schema: s,
|
schema,
|
||||||
parser: createMarkdownParser(s, plugins),
|
parser: createMarkdownParser(schema, plugins),
|
||||||
serializer: createSerializer(s, plugins),
|
serializer: createSerializer(schema, plugins),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,7 +129,7 @@ export default class Editor extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleAction = (action) => {
|
handleAction = (action) => {
|
||||||
const { schema, serializer } = this.state;
|
const { serializer } = this.state;
|
||||||
const newState = this.view.state.applyAction(action);
|
const newState = this.view.state.applyAction(action);
|
||||||
const md = serializer.serialize(newState.doc);
|
const md = serializer.serialize(newState.doc);
|
||||||
this.props.onChange(md);
|
this.props.onChange(md);
|
||||||
@ -152,15 +148,13 @@ export default class Editor extends Component {
|
|||||||
const pos = this.view.coordsAtPos(selection.from);
|
const pos = this.view.coordsAtPos(selection.from);
|
||||||
const editorPos = this.view.content.getBoundingClientRect();
|
const editorPos = this.view.content.getBoundingClientRect();
|
||||||
const selectionPosition = { top: pos.top - editorPos.top, left: pos.left - editorPos.left };
|
const selectionPosition = { top: pos.top - editorPos.top, left: pos.left - editorPos.left };
|
||||||
this.setState({ showToolbar: false, showBlockMenu: true, selectionPosition });
|
this.setState({ selectionPosition });
|
||||||
} else {
|
|
||||||
this.setState({ showToolbar: false, showBlockMenu: false });
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const pos = this.view.coordsAtPos(selection.from);
|
const pos = this.view.coordsAtPos(selection.from);
|
||||||
const editorPos = this.view.content.getBoundingClientRect();
|
const editorPos = this.view.content.getBoundingClientRect();
|
||||||
const selectionPosition = { top: pos.top - editorPos.top, left: pos.left - editorPos.left };
|
const selectionPosition = { top: pos.top - editorPos.top, left: pos.left - editorPos.left };
|
||||||
this.setState({ showToolbar: true, showBlockMenu: false, selectionPosition });
|
this.setState({ selectionPosition });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -202,13 +196,13 @@ export default class Editor extends Component {
|
|||||||
handleLink = () => {
|
handleLink = () => {
|
||||||
let url = null;
|
let url = null;
|
||||||
if (!markActive(this.view.state, this.state.schema.marks.link)) {
|
if (!markActive(this.view.state, this.state.schema.marks.link)) {
|
||||||
url = prompt('Link URL:');
|
url = prompt('Link URL:'); // eslint-disable-line no-alert
|
||||||
}
|
}
|
||||||
const command = toggleMark(this.state.schema.marks.link, { href: url ? processUrl(url) : null });
|
const command = toggleMark(this.state.schema.marks.link, { href: url ? processUrl(url) : null });
|
||||||
command(this.view.state, this.handleAction);
|
command(this.view.state, this.handleAction);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleBlock = (plugin, data) => {
|
handlePluginSubmit = (plugin, data) => {
|
||||||
const { schema } = this.state;
|
const { schema } = this.state;
|
||||||
const nodeType = schema.nodes[`plugin_${ plugin.get('id') }`];
|
const nodeType = schema.nodes[`plugin_${ plugin.get('id') }`];
|
||||||
this.view.props.onAction(this.view.state.tr.replaceSelectionWith(nodeType.create(data.toJS())).action());
|
this.view.props.onAction(this.view.state.tr.replaceSelectionWith(nodeType.create(data.toJS())).action());
|
||||||
@ -268,7 +262,7 @@ export default class Editor extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { onAddAsset, onRemoveAsset, getAsset } = this.props;
|
const { onAddAsset, onRemoveAsset, getAsset } = this.props;
|
||||||
const { plugins, showToolbar, showBlockMenu, selectionPosition, dragging } = this.state;
|
const { plugins, selectionPosition, dragging } = this.state;
|
||||||
const classNames = [styles.editor];
|
const classNames = [styles.editor];
|
||||||
if (dragging) {
|
if (dragging) {
|
||||||
classNames.push(styles.dragging);
|
classNames.push(styles.dragging);
|
||||||
@ -281,8 +275,8 @@ export default class Editor extends Component {
|
|||||||
onDragOver={this.handleDragOver}
|
onDragOver={this.handleDragOver}
|
||||||
onDrop={this.handleDrop}
|
onDrop={this.handleDrop}
|
||||||
>
|
>
|
||||||
|
<Sticky className={styles.editorControlBar} fillContainerWidth>
|
||||||
<Toolbar
|
<Toolbar
|
||||||
isOpen={showToolbar}
|
|
||||||
selectionPosition={selectionPosition}
|
selectionPosition={selectionPosition}
|
||||||
onH1={this.handleHeader(1)}
|
onH1={this.handleHeader(1)}
|
||||||
onH2={this.handleHeader(2)}
|
onH2={this.handleHeader(2)}
|
||||||
@ -290,16 +284,13 @@ export default class Editor extends Component {
|
|||||||
onItalic={this.handleItalic}
|
onItalic={this.handleItalic}
|
||||||
onLink={this.handleLink}
|
onLink={this.handleLink}
|
||||||
onToggleMode={this.handleToggle}
|
onToggleMode={this.handleToggle}
|
||||||
/>
|
|
||||||
<BlockMenu
|
|
||||||
isOpen={showBlockMenu}
|
|
||||||
selectionPosition={selectionPosition}
|
|
||||||
plugins={plugins}
|
plugins={plugins}
|
||||||
onBlock={this.handleBlock}
|
onSubmit={this.handlePluginSubmit}
|
||||||
onAddAsset={onAddAsset}
|
onAddAsset={onAddAsset}
|
||||||
onRemoveAsset={onRemoveAsset}
|
onRemoveAsset={onRemoveAsset}
|
||||||
getAsset={getAsset}
|
getAsset={getAsset}
|
||||||
/>
|
/>
|
||||||
|
</Sticky>
|
||||||
<div ref={this.handleRef} />
|
<div ref={this.handleRef} />
|
||||||
<div className={styles.shim} />
|
<div className={styles.shim} />
|
||||||
</div>);
|
</div>);
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
export const emptyParagraphBlock = {
|
|
||||||
nodes: [
|
|
||||||
{
|
|
||||||
kind: 'block',
|
|
||||||
type: 'paragraph',
|
|
||||||
nodes: [{
|
|
||||||
kind: 'text',
|
|
||||||
ranges: [{
|
|
||||||
text: '',
|
|
||||||
}],
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mediaproxyBlock = mediaproxy => ({
|
|
||||||
kind: 'block',
|
|
||||||
type: 'paragraph',
|
|
||||||
nodes: [{
|
|
||||||
kind: 'inline',
|
|
||||||
type: 'mediaproxy',
|
|
||||||
isVoid: true,
|
|
||||||
data: {
|
|
||||||
alt: mediaproxy.name,
|
|
||||||
src: mediaproxy.public_path,
|
|
||||||
},
|
|
||||||
}],
|
|
||||||
});
|
|
@ -45,7 +45,7 @@ const buildtInPlugins = [{
|
|||||||
},
|
},
|
||||||
toBlock: data => ``,
|
toBlock: data => ``,
|
||||||
toPreview: data => <img src={data.image} alt={data.alt} />,
|
toPreview: data => <img src={data.image} alt={data.alt} />,
|
||||||
pattern: /^!\[([^\]]+)\]\(([^\)]+)\)$/,
|
pattern: /^!\[([^\]]+)]\(([^)]+)\)$/,
|
||||||
fields: [{
|
fields: [{
|
||||||
label: 'Image',
|
label: 'Image',
|
||||||
name: 'image',
|
name: 'image',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user