condense rich text functionality to static toolbar
This commit is contained in:
parent
96453df346
commit
b2fd96c12e
@ -8,16 +8,23 @@
|
||||
--successColor: #1c7;
|
||||
--warningColor: #fa0;
|
||||
--errorColor: #f52;
|
||||
--textColor: #272e30;
|
||||
--borderRadius: 2px;
|
||||
--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;
|
||||
--foregroundAltColor: #fff;
|
||||
--backgroundAltColor: #272e30;
|
||||
--textFieldBorderColor: #e7e7e7;
|
||||
--highlightFGColor: #fff;
|
||||
--highlightBGColor: #3ab7a5;
|
||||
--controlLabelColor: #272e30;
|
||||
--controlLabelColor: var(--textColor);
|
||||
--controlBGColor: #fff;
|
||||
--backgroundTertiaryColor: #f2f5f4;
|
||||
--backgroundTertiaryColorDark: color(var(--backgroundTertiaryColor) lightness(90%));
|
||||
}
|
||||
|
||||
.base {
|
||||
|
@ -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;
|
||||
}
|
@ -2,6 +2,10 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.editorControlBar {
|
||||
composes: editorControlBar from "../VisualEditor/index.css";
|
||||
}
|
||||
|
||||
.dragging { }
|
||||
|
||||
.shim {
|
||||
|
@ -6,7 +6,7 @@ import CaretPosition from 'textarea-caret-position';
|
||||
import registry from '../../../../lib/registry';
|
||||
import { createAssetProxy } from '../../../../valueObjects/AssetProxy';
|
||||
import Toolbar from '../Toolbar';
|
||||
import BlockMenu from '../BlockMenu';
|
||||
import ToolbarPlugins from '../ToolbarPlugins';
|
||||
import styles from './index.css';
|
||||
|
||||
const HAS_LINE_BREAK = /\n/m;
|
||||
@ -18,7 +18,7 @@ function processUrl(url) {
|
||||
if (url.match(/^(https?:\/\/|mailto:|\/)/)) {
|
||||
return url;
|
||||
}
|
||||
if (url.match(/^[^\/]+\.[^\/]+/)) {
|
||||
if (url.match(/^[^/]+\.[^/]+/)) {
|
||||
return `https://${ url }`;
|
||||
}
|
||||
return `/${ url }`;
|
||||
@ -44,7 +44,9 @@ function getCleanPaste(e) {
|
||||
// Handle complex pastes by stealing focus with a contenteditable div
|
||||
const div = document.createElement('div');
|
||||
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);
|
||||
div.focus();
|
||||
setTimeout(() => {
|
||||
@ -194,7 +196,7 @@ export default class RawEditor extends React.Component {
|
||||
};
|
||||
|
||||
handleLink = () => {
|
||||
const url = prompt('URL:');
|
||||
const url = prompt('URL:'); // eslint-disable-line no-alert
|
||||
const selection = this.getSelection();
|
||||
this.replaceSelection(`[${ selection.selected }](${ processUrl(url) })`);
|
||||
};
|
||||
@ -205,9 +207,9 @@ export default class RawEditor extends React.Component {
|
||||
if (selection.start !== selection.end && !HAS_LINE_BREAK.test(selection.selected)) {
|
||||
try {
|
||||
const selectionPosition = this.caretPosition.get(selection.start, selection.end);
|
||||
this.setState({ showToolbar: true, showBlockMenu: false, selectionPosition });
|
||||
this.setState({ selectionPosition });
|
||||
} catch (e) {
|
||||
this.setState({ showToolbar: false, showBlockMenu: false });
|
||||
console.log(e); // eslint-disable-line no-console
|
||||
}
|
||||
} else if (selection.start === selection.end) {
|
||||
const newBlock =
|
||||
@ -222,12 +224,8 @@ export default class RawEditor extends React.Component {
|
||||
|
||||
if (newBlock) {
|
||||
const position = this.caretPosition.get(selection.start, selection.end);
|
||||
this.setState({ showToolbar: false, showBlockMenu: true, selectionPosition: position });
|
||||
} else {
|
||||
this.setState({ showToolbar: false, showBlockMenu: false });
|
||||
this.setState({ selectionPosition: position });
|
||||
}
|
||||
} else {
|
||||
this.setState({ showToolbar: false, showBlockMenu: false });
|
||||
}
|
||||
};
|
||||
|
||||
@ -236,10 +234,9 @@ export default class RawEditor extends React.Component {
|
||||
this.updateHeight();
|
||||
};
|
||||
|
||||
handleBlock = (plugin, data) => {
|
||||
handlePlugin = (plugin, data) => {
|
||||
const toBlock = plugin.get('toBlock');
|
||||
this.replaceSelection(toBlock.call(toBlock, data.toJS()));
|
||||
this.setState({ showBlockMenu: false });
|
||||
};
|
||||
|
||||
handleHeader(header) {
|
||||
@ -309,7 +306,7 @@ export default class RawEditor extends React.Component {
|
||||
|
||||
render() {
|
||||
const { onAddAsset, onRemoveAsset, getAsset } = this.props;
|
||||
const { showToolbar, showBlockMenu, plugins, selectionPosition, dragging } = this.state;
|
||||
const { plugins, selectionPosition, dragging } = this.state;
|
||||
const classNames = [styles.root];
|
||||
if (dragging) {
|
||||
classNames.push(styles.dragging);
|
||||
@ -322,25 +319,26 @@ export default class RawEditor extends React.Component {
|
||||
onDragOver={this.handleDragOver}
|
||||
onDrop={this.handleDrop}
|
||||
>
|
||||
<Toolbar
|
||||
isOpen={showToolbar}
|
||||
selectionPosition={selectionPosition}
|
||||
onH1={this.handleHeader('#')}
|
||||
onH2={this.handleHeader('##')}
|
||||
onBold={this.handleBold}
|
||||
onItalic={this.handleItalic}
|
||||
onLink={this.handleLink}
|
||||
onToggleMode={this.handleToggle}
|
||||
/>
|
||||
<BlockMenu
|
||||
isOpen={showBlockMenu}
|
||||
selectionPosition={selectionPosition}
|
||||
plugins={plugins}
|
||||
onBlock={this.handleBlock}
|
||||
onAddAsset={onAddAsset}
|
||||
onRemoveAsset={onRemoveAsset}
|
||||
getAsset={getAsset}
|
||||
/>
|
||||
<div className={styles.editorControlBar}>
|
||||
<Toolbar
|
||||
selectionPosition={selectionPosition}
|
||||
onH1={this.handleHeader('#')}
|
||||
onH2={this.handleHeader('##')}
|
||||
onBold={this.handleBold}
|
||||
onItalic={this.handleItalic}
|
||||
onLink={this.handleLink}
|
||||
onToggleMode={this.handleToggle}
|
||||
/>
|
||||
<ToolbarPlugins
|
||||
selectionPosition={selectionPosition}
|
||||
plugins={plugins}
|
||||
onPlugin={this.handlePlugin}
|
||||
onAddAsset={onAddAsset}
|
||||
onRemoveAsset={onRemoveAsset}
|
||||
getAsset={getAsset}
|
||||
rawMode
|
||||
/>
|
||||
</div>
|
||||
<textarea
|
||||
ref={this.handleRef}
|
||||
value={this.props.value || ''}
|
||||
|
@ -1,28 +1,20 @@
|
||||
.Toolbar {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
margin: none;
|
||||
padding: none;
|
||||
box-shadow: 1px 1px 5px;
|
||||
list-style: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.Button {
|
||||
display: inline-block;
|
||||
|
||||
& button {
|
||||
padding: 5px;
|
||||
padding: 6px;
|
||||
border: none;
|
||||
border-right: 1px solid #eee;
|
||||
background: #fff;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #eee;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Button:last-child button {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.Visible {
|
||||
display: block;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Icon } from '../../UI';
|
||||
import styles from './Toolbar.css';
|
||||
|
||||
@ -10,54 +10,27 @@ function button(label, icon, action) {
|
||||
</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>
|
||||
);
|
||||
}
|
||||
function Toolbar(props) {
|
||||
const { onH1, onH2, onBold, onItalic, onLink, onToggleMode } = props;
|
||||
return (
|
||||
<ul className={styles.Toolbar}>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
Toolbar.propTypes = {
|
||||
onH1: PropTypes.func.isRequired,
|
||||
onH2: PropTypes.func.isRequired,
|
||||
onBold: PropTypes.func.isRequired,
|
||||
onItalic: PropTypes.func.isRequired,
|
||||
onLink: PropTypes.func.isRequired,
|
||||
onToggleMode: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Toolbar;
|
||||
|
@ -0,0 +1,62 @@
|
||||
@import "../../UI/theme";
|
||||
|
||||
.root {
|
||||
z-index: 1000;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border: 1px solid #444;
|
||||
border-radius: 100%;
|
||||
background: transparent;
|
||||
line-height: 13px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.menu {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.menuItem {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.control {
|
||||
composes: control from "../../ControlPanel/ControlPane.css"
|
||||
}
|
||||
|
||||
.label {
|
||||
composes: label from "../../ControlPanel/ControlPane.css";
|
||||
}
|
||||
|
||||
.footer {
|
||||
background-color: var(--backgroundColor);
|
||||
padding: 10px 20px;
|
||||
text-align: right;
|
||||
}
|
@ -1,15 +1,15 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { fromJS } from 'immutable';
|
||||
import { Button } from 'react-toolbox/lib/button';
|
||||
import { Icon } from '../../UI';
|
||||
import { resolveWidget } from '../../Widgets';
|
||||
import styles from './BlockMenu.css';
|
||||
import toolbarStyles from './Toolbar.css';
|
||||
import styles from './ToolbarPlugins.css';
|
||||
|
||||
export default class BlockMenu extends Component {
|
||||
export default class ToolbarPlugins extends Component {
|
||||
static propTypes = {
|
||||
isOpen: PropTypes.bool,
|
||||
selectionPosition: PropTypes.object,
|
||||
plugins: PropTypes.object.isRequired,
|
||||
onBlock: PropTypes.func.isRequired,
|
||||
onPlugin: PropTypes.func.isRequired,
|
||||
onAddAsset: PropTypes.func.isRequired,
|
||||
onRemoveAsset: PropTypes.func.isRequired,
|
||||
getAsset: PropTypes.func.isRequired,
|
||||
@ -18,29 +18,11 @@ export default class BlockMenu extends Component {
|
||||
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();
|
||||
@ -49,21 +31,23 @@ export default class BlockMenu extends Component {
|
||||
}
|
||||
|
||||
buttonFor(plugin) {
|
||||
return (<li key={`plugin-${ plugin.get('id') }`}>
|
||||
<button onClick={this.handlePlugin(plugin)}>{plugin.get('label')}</button>
|
||||
return (<li key={`plugin-${ plugin.get('id') }`} className={toolbarStyles.Button}>
|
||||
<button className={styles[plugin.get('label')]} onClick={this.handlePlugin(plugin)} title={plugin.get('label')}>
|
||||
<Icon type={plugin.get('icon')} />
|
||||
</button>
|
||||
</li>);
|
||||
}
|
||||
|
||||
handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const { openPlugin, pluginData } = this.state;
|
||||
this.props.onBlock(openPlugin, pluginData);
|
||||
this.setState({ openPlugin: null, isExpanded: false });
|
||||
this.props.onPlugin(openPlugin, pluginData);
|
||||
this.setState({ openPlugin: null });
|
||||
};
|
||||
|
||||
handleCancel = (e) => {
|
||||
e.preventDefault();
|
||||
this.setState({ openPlugin: null, isExpanded: false });
|
||||
this.setState({ openPlugin: null });
|
||||
};
|
||||
|
||||
controlFor(field) {
|
||||
@ -71,10 +55,11 @@ export default class BlockMenu extends Component {
|
||||
const { pluginData } = this.state;
|
||||
const widget = resolveWidget(field.get('widget') || 'string');
|
||||
const value = pluginData.get(field.get('name'));
|
||||
const key = `field-${ field.get('name') }`;
|
||||
|
||||
return (
|
||||
<div className={styles.control} key={`field-${ field.get('name') }`}>
|
||||
<label className={styles.label}>{field.get('label')}</label>
|
||||
<div className={styles.control} key={key}>
|
||||
<label className={styles.label} htmlFor={key}>{field.get('label')}</label>
|
||||
{
|
||||
React.createElement(widget.control, {
|
||||
field,
|
||||
@ -95,8 +80,10 @@ export default class BlockMenu extends Component {
|
||||
|
||||
pluginForm(plugin) {
|
||||
return (<form className={styles.pluginForm} onSubmit={this.handleSubmit}>
|
||||
<h3>Insert {plugin.get('label')}</h3>
|
||||
{plugin.get('fields').map(field => this.controlFor(field))}
|
||||
<h3 className={styles.header}>Insert {plugin.get('label')}</h3>
|
||||
<div className={styles.body}>
|
||||
{plugin.get('fields').map(field => this.controlFor(field))}
|
||||
</div>
|
||||
<div className={styles.footer}>
|
||||
<Button
|
||||
raised
|
||||
@ -113,19 +100,16 @@ export default class BlockMenu extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isOpen, plugins } = this.props;
|
||||
const { isExpanded, openPlugin } = this.state;
|
||||
const { plugins } = this.props;
|
||||
const { 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(' ')}>
|
||||
return (<div className={classNames.join(' ')}>
|
||||
<ul className={styles.menu}>
|
||||
{plugins.map(plugin => this.buttonFor(plugin))}
|
||||
</ul>
|
||||
{openPlugin && this.pluginForm(openPlugin)}
|
@ -1,5 +1,11 @@
|
||||
@import "../../../UI/theme";
|
||||
|
||||
.editorControlBar {
|
||||
background-color: var(--controlBGColor);
|
||||
margin-bottom: 1px;
|
||||
border-radius: var(--borderRadius) var(--borderRadius) 0 0;
|
||||
}
|
||||
|
||||
.editor {
|
||||
position: relative;
|
||||
& h1, & h2, & h3 {
|
||||
@ -61,7 +67,11 @@
|
||||
position: relative;
|
||||
background-color: var(--controlBGColor);
|
||||
padding: 12px;
|
||||
border-radius: var(--borderRadius);
|
||||
border-radius: 0 0 var(--borderRadius) var(--borderRadius);
|
||||
|
||||
& ul {
|
||||
padding-left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
& .ProseMirror-content {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { Map } from 'immutable';
|
||||
import { Schema } from 'prosemirror-model';
|
||||
import { EditorState } from 'prosemirror-state';
|
||||
import { EditorView } from 'prosemirror-view';
|
||||
@ -8,21 +9,21 @@ import {
|
||||
inputRules, allInputRules,
|
||||
} from 'prosemirror-inputrules';
|
||||
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 registry from '../../../../lib/registry';
|
||||
import { createAssetProxy } from '../../../../valueObjects/AssetProxy';
|
||||
import { buildKeymap } from './keymap';
|
||||
import createMarkdownParser from './parser';
|
||||
import Toolbar from '../Toolbar';
|
||||
import BlockMenu from '../BlockMenu';
|
||||
import ToolbarPlugins from '../ToolbarPlugins';
|
||||
import styles from './index.css';
|
||||
|
||||
function processUrl(url) {
|
||||
if (url.match(/^(https?:\/\/|mailto:|\/)/)) {
|
||||
return url;
|
||||
}
|
||||
if (url.match(/^[^\/]+\.[^\/]+/)) {
|
||||
if (url.match(/^[^/]+\.[^/]+/)) {
|
||||
return `https://${ url }`;
|
||||
}
|
||||
return `/${ url }`;
|
||||
@ -37,15 +38,10 @@ const ruleset = {
|
||||
};
|
||||
|
||||
function buildInputRules(schema) {
|
||||
const result = [];
|
||||
for (const rule in ruleset) {
|
||||
const type = schema.nodes[rule];
|
||||
if (type) {
|
||||
const fn = ruleset[rule];
|
||||
result.push(fn[0].apply(fn.slice(1)));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
return Map(ruleset)
|
||||
.filter(rule => schema.nodes[rule])
|
||||
.map(rule => rule[0].apply(rule[0].slice(1)))
|
||||
.toArray();
|
||||
}
|
||||
|
||||
function markActive(state, type) {
|
||||
@ -99,12 +95,12 @@ export default class Editor extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const plugins = registry.getEditorComponents();
|
||||
const s = schemaWithPlugins(schema, plugins);
|
||||
const schema = schemaWithPlugins(markdownSchema, plugins);
|
||||
this.state = {
|
||||
plugins,
|
||||
schema: s,
|
||||
parser: createMarkdownParser(s, plugins),
|
||||
serializer: createSerializer(s, plugins),
|
||||
schema,
|
||||
parser: createMarkdownParser(schema, plugins),
|
||||
serializer: createSerializer(schema, plugins),
|
||||
};
|
||||
}
|
||||
|
||||
@ -133,7 +129,7 @@ export default class Editor extends Component {
|
||||
}
|
||||
|
||||
handleAction = (action) => {
|
||||
const { schema, serializer } = this.state;
|
||||
const { serializer } = this.state;
|
||||
const newState = this.view.state.applyAction(action);
|
||||
const md = serializer.serialize(newState.doc);
|
||||
this.props.onChange(md);
|
||||
@ -152,15 +148,13 @@ export default class Editor extends Component {
|
||||
const pos = this.view.coordsAtPos(selection.from);
|
||||
const editorPos = this.view.content.getBoundingClientRect();
|
||||
const selectionPosition = { top: pos.top - editorPos.top, left: pos.left - editorPos.left };
|
||||
this.setState({ showToolbar: false, showBlockMenu: true, selectionPosition });
|
||||
} else {
|
||||
this.setState({ showToolbar: false, showBlockMenu: false });
|
||||
this.setState({ selectionPosition });
|
||||
}
|
||||
} else {
|
||||
const pos = this.view.coordsAtPos(selection.from);
|
||||
const editorPos = this.view.content.getBoundingClientRect();
|
||||
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 = () => {
|
||||
let url = null;
|
||||
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 });
|
||||
command(this.view.state, this.handleAction);
|
||||
};
|
||||
|
||||
handleBlock = (plugin, data) => {
|
||||
handlePlugin = (plugin, data) => {
|
||||
const { schema } = this.state;
|
||||
const nodeType = schema.nodes[`plugin_${ plugin.get('id') }`];
|
||||
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() {
|
||||
const { onAddAsset, onRemoveAsset, getAsset } = this.props;
|
||||
const { plugins, showToolbar, showBlockMenu, selectionPosition, dragging } = this.state;
|
||||
const { plugins, selectionPosition, dragging } = this.state;
|
||||
const classNames = [styles.editor];
|
||||
if (dragging) {
|
||||
classNames.push(styles.dragging);
|
||||
@ -281,25 +275,25 @@ export default class Editor extends Component {
|
||||
onDragOver={this.handleDragOver}
|
||||
onDrop={this.handleDrop}
|
||||
>
|
||||
<Toolbar
|
||||
isOpen={showToolbar}
|
||||
selectionPosition={selectionPosition}
|
||||
onH1={this.handleHeader(1)}
|
||||
onH2={this.handleHeader(2)}
|
||||
onBold={this.handleBold}
|
||||
onItalic={this.handleItalic}
|
||||
onLink={this.handleLink}
|
||||
onToggleMode={this.handleToggle}
|
||||
/>
|
||||
<BlockMenu
|
||||
isOpen={showBlockMenu}
|
||||
selectionPosition={selectionPosition}
|
||||
plugins={plugins}
|
||||
onBlock={this.handleBlock}
|
||||
onAddAsset={onAddAsset}
|
||||
onRemoveAsset={onRemoveAsset}
|
||||
getAsset={getAsset}
|
||||
/>
|
||||
<div className={styles.editorControlBar}>
|
||||
<Toolbar
|
||||
selectionPosition={selectionPosition}
|
||||
onH1={this.handleHeader(1)}
|
||||
onH2={this.handleHeader(2)}
|
||||
onBold={this.handleBold}
|
||||
onItalic={this.handleItalic}
|
||||
onLink={this.handleLink}
|
||||
onToggleMode={this.handleToggle}
|
||||
/>
|
||||
<ToolbarPlugins
|
||||
selectionPosition={selectionPosition}
|
||||
plugins={plugins}
|
||||
onPlugin={this.handlePlugin}
|
||||
onAddAsset={onAddAsset}
|
||||
onRemoveAsset={onRemoveAsset}
|
||||
getAsset={getAsset}
|
||||
/>
|
||||
</div>
|
||||
<div ref={this.handleRef} />
|
||||
<div className={styles.shim} />
|
||||
</div>);
|
||||
|
@ -39,13 +39,14 @@ if (process.env.NODE_ENV !== 'production' && module.hot) {
|
||||
const buildtInPlugins = [{
|
||||
label: 'Image',
|
||||
id: 'image',
|
||||
icon: 'picture',
|
||||
fromBlock: match => match && {
|
||||
image: match[2],
|
||||
alt: match[1],
|
||||
},
|
||||
toBlock: data => ``,
|
||||
toPreview: data => <img src={data.image} alt={data.alt} />,
|
||||
pattern: /^!\[([^\]]+)\]\(([^\)]+)\)$/,
|
||||
pattern: /^!\[([^\]]+)]\(([^)]+)\)$/,
|
||||
fields: [{
|
||||
label: 'Image',
|
||||
name: 'image',
|
||||
|
Loading…
x
Reference in New Issue
Block a user