diff --git a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.css b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.css index 8786390c..d200d47b 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.css +++ b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.css @@ -12,25 +12,6 @@ composes: editorControlBarSticky from "../VisualEditor/index.css"; } -.dragging { } - -.shim { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - display: none; - border: 2px dashed #aaa; - background: rgba(0,0,0,0.2); -} - -.dragging .shim { - z-index: 1000; - display: block; - pointer-events: none; -} - .textarea { overflow: hidden; resize: none; diff --git a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js index ad05396a..a812c004 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js @@ -1,365 +1,49 @@ import React, { PropTypes } from 'react'; -import get from 'lodash/get'; -import unified from 'unified'; -import CaretPosition from 'textarea-caret-position'; import TextareaAutosize from 'react-textarea-autosize'; -import registry from '../../../../../lib/registry'; import { markdownToHtml, htmlToMarkdown } from '../../unified'; -import { createAssetProxy } from '../../../../../valueObjects/AssetProxy'; import Toolbar from '../Toolbar/Toolbar'; import { Sticky } from '../../../../UI/Sticky/Sticky'; import styles from './index.css'; -const HAS_LINE_BREAK = /\n/m; - -function processUrl(url) { - if (url.match(/^(https?:\/\/|mailto:|\/)/)) { - return url; - } - if (url.match(/^[^/]+\.[^/]+/)) { - return `https://${ url }`; - } - return `/${ url }`; -} - -function getCleanPaste(e) { - const transfer = e.clipboardData; - return new Promise((resolve) => { - const isHTML = !!Array.from(transfer.types).find(type => type === 'text/html'); - - if (isHTML) { - const data = transfer.getData('text/html'); - // Avoid trying to clean up full HTML documents with head/body/etc - if (!data.match(/^\s*<!doctype/i)) { - e.preventDefault(); - resolve(htmlToMarkdown(data)); - } else { - // 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;' - ); - document.body.appendChild(div); - div.focus(); - setTimeout(() => { - resolve(htmlToMarkdown(div.innerHTML)); - document.body.removeChild(div); - }, 50); - return null; - } - } - - e.preventDefault(); - return resolve(transfer.getData(transfer.types[0])); - }); -} - export default class RawEditor extends React.Component { constructor(props) { super(props); - const plugins = registry.getEditorComponents(); this.state = { - value: htmlToMarkdown(this.props.value), - plugins, - }; - this.shortcuts = { - meta: { - b: this.handleBold, - i: this.handleItalic, - }, + value: htmlToMarkdown(this.props.value) || '', }; } - componentDidMount() { - this.updateHeight(); - this.element.addEventListener('paste', this.handlePaste, false); - } - - componentDidUpdate() { - if (this.newSelection) { - this.element.selectionStart = this.newSelection.start; - this.element.selectionEnd = this.newSelection.end; - this.newSelection = null; - } - } - - componentWillUnmount() { - this.element.removeEventListener('paste', this.handlePaste); - } - - getSelection() { - const start = this.element.selectionStart; - const end = this.element.selectionEnd; - const selected = (this.state.value || '').substr(start, end - start); - return { start, end, selected }; - } - - surroundSelection(chars) { - const selection = this.getSelection(); - const newSelection = Object.assign({}, selection); - const { value } = this.state; - const escapedChars = chars.replace(/\*/g, '\\*'); - const regexp = new RegExp(`^${ escapedChars }.*${ escapedChars }$`); - let changed = chars + selection.selected + chars; - - if (regexp.test(selection.selected)) { - changed = selection.selected.substr(chars.length, selection.selected.length - (chars.length * 2)); - newSelection.end = selection.end - (chars.length * 2); - } else if ( - value.substr(selection.start - chars.length, chars.length) === chars && - value.substr(selection.end, chars.length) === chars - ) { - newSelection.start = selection.start - chars.length; - newSelection.end = selection.end + chars.length; - changed = selection.selected; - } else { - newSelection.end = selection.end + (chars.length * 2); - } - - const beforeSelection = value.substr(0, selection.start); - const afterSelection = value.substr(selection.end); - - this.newSelection = newSelection; - this.handleChange(beforeSelection + changed + afterSelection); - } - - replaceSelection(chars) { - const value = this.state.value || ''; - const selection = this.getSelection(); - const newSelection = Object.assign({}, selection); - const beforeSelection = value.substr(0, selection.start); - const afterSelection = value.substr(selection.end); - newSelection.end = selection.start + chars.length; - this.newSelection = newSelection; - this.handleChange(beforeSelection + chars + afterSelection); - } - - toggleHeader(header) { - const value = this.state.value || ''; - const selection = this.getSelection(); - const newSelection = Object.assign({}, selection); - const lastNewline = value.lastIndexOf('\n', selection.start); - const currentMatch = value.substr(lastNewline + 1).match(/^(#+)\s/); - const beforeHeader = value.substr(0, lastNewline + 1); - let afterHeader; - let chars; - if (currentMatch) { - afterHeader = value.substr(lastNewline + 1 + currentMatch[0].length); - chars = currentMatch[1] === header ? '' : `${ header } `; - const diff = chars.length - currentMatch[0].length; - newSelection.start += diff; - newSelection.end += diff; - } else { - afterHeader = value.substr(lastNewline + 1); - chars = `${ header } `; - newSelection.start += header.length + 1; - newSelection.end += header.length + 1; - } - this.newSelection = newSelection; - this.handleChange(beforeHeader + chars + afterHeader); - } - - updateHeight() { - if (this.element.scrollHeight > this.element.clientHeight) { - this.element.style.height = `${ this.element.scrollHeight }px`; - } - } - - handleRef = (ref) => { - this.element = ref; - if (ref) { - this.caretPosition = new CaretPosition(ref); - } - }; - - handleKey = (e) => { - if (e.metaKey) { - const action = this.shortcuts.meta[e.key]; - if (action) { - e.preventDefault(); - action(); - } - } - }; - - handleBold = () => { - this.surroundSelection('**'); - }; - - handleItalic = () => { - this.surroundSelection('*'); - }; - - handleLink = () => { - const url = prompt('URL:'); // eslint-disable-line no-alert - const selection = this.getSelection(); - this.replaceSelection(`[${ selection.selected }](${ processUrl(url) })`); - }; - - handleSelection = () => { - const value = this.state.value || ''; - const selection = this.getSelection(); - if (selection.start !== selection.end && !HAS_LINE_BREAK.test(selection.selected)) { - try { - const selectionPosition = this.caretPosition.get(selection.start, selection.end); - this.setState({ selectionPosition }); - } catch (e) { - console.log(e); // eslint-disable-line no-console - } - } else if (selection.start === selection.end) { - const newBlock = - ( - (selection.start === 0 && value.substr(0, 1).match(/^\n?$/)) || - value.substr(selection.start - 2, 2) === '\n\n') && - ( - selection.end === (value.length - 1) || - value.substr(selection.end, 2) === '\n\n' || - value.substr(selection.end).match(/\n*$/m) - ); - - if (newBlock) { - const position = this.caretPosition.get(selection.start, selection.end); - this.setState({ selectionPosition: position }); - } - } - }; - handleChange = (e) => { - // handleChange may receive an event or a value - const value = typeof e === 'object' ? e.target.value : e; - const html = markdownToHtml(value); + const html = markdownToHtml(e.target.value); this.props.onChange(html); - this.updateHeight(); - this.setState({ value }); + this.setState({ value: e.target.value }); }; - handlePluginSubmit = (plugin, data) => { - const toBlock = plugin.get('toBlock'); - this.replaceSelection(toBlock.call(toBlock, data.toJS())); - }; - - handleHeader(header) { - return () => { - this.toggleHeader(header); - }; - } - - handleDragEnter = (e) => { - e.preventDefault(); - this.setState({ dragging: true }); - }; - - handleDragLeave = (e) => { - e.preventDefault(); - this.setState({ dragging: false }); - }; - - handleDragOver = (e) => { - e.preventDefault(); - }; - - handleDrop = (e) => { - e.preventDefault(); - - this.setState({ dragging: false }); - - let data; - - if (e.dataTransfer.files && e.dataTransfer.files.length) { - data = Array.from(e.dataTransfer.files).map((file) => { - const link = `[Uploading ${ file.name }...]()`; - if (file.type.split('/')[0] === 'image') { - return `!${ link }`; - } - - createAssetProxy(file.name, file) - .then((assetProxy) => { - this.props.onAddAsset(assetProxy); - // TODO: Change the link text - }); - return link; - }).join('\n\n'); - } else { - data = e.dataTransfer.getData('text/plain'); - } - this.replaceSelection(data); - }; - - handlePaste = (e) => { - const { value } = this.state; - const selection = this.getSelection(); - const beforeSelection = value.substr(0, selection.start); - const afterSelection = value.substr(selection.end); - - getCleanPaste(e).then((paste) => { - const newSelection = Object.assign({}, selection); - newSelection.start = newSelection.end = beforeSelection.length + paste.length; - this.newSelection = newSelection; - this.handleChange(beforeSelection + paste + afterSelection); - }); - }; - - handleToggle = () => { + handleToggleMode = () => { this.props.onMode('visual'); }; render() { - const { onAddAsset, onRemoveAsset, getAsset } = this.props; - const { plugins, selectionPosition, dragging } = this.state; - const classNames = [styles.root]; - if (dragging) { - classNames.push(styles.dragging); - } - - return (<div - className={classNames.join(' ')} - onDragEnter={this.handleDragEnter} - onDragLeave={this.handleDragLeave} - onDragOver={this.handleDragOver} - onDrop={this.handleDrop} - > - <Sticky - className={styles.editorControlBar} - classNameActive={styles.editorControlBarSticky} - fillContainerWidth - > - <Toolbar - selectionPosition={selectionPosition} - buttons={{ - h1: { onAction: this.handleHeader('#') }, - h2: { onAction: this.handleHeader('##') }, - bold: { onAction: this.handleBold }, - italic: { onAction: this.handleItalic }, - link: { onAction: this.handleLink }, - }} - onToggleMode={this.handleToggle} - plugins={plugins} - onSubmit={this.handlePluginSubmit} - onAddAsset={onAddAsset} - onRemoveAsset={onRemoveAsset} - getAsset={getAsset} - rawMode + return ( + <div className={styles.root}> + <Sticky + className={styles.editorControlBar} + classNameActive={styles.editorControlBarSticky} + fillContainerWidth + > + <Toolbar onToggleMode={this.handleToggleMode} disabled rawMode /> + </Sticky> + <TextareaAutosize + className={styles.textarea} + value={this.state.value} + onChange={this.handleChange} /> - </Sticky> - <TextareaAutosize - className={styles.textarea} - inputRef={this.handleRef} - className={styles.textarea} - value={this.state.value || ''} - onKeyDown={this.handleKey} - onChange={this.handleChange} - onSelect={this.handleSelection} - /> - <div className={styles.shim} /> - </div>); + </div> + ); } } RawEditor.propTypes = { - onAddAsset: PropTypes.func.isRequired, - onRemoveAsset: PropTypes.func.isRequired, - getAsset: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired, onMode: PropTypes.func.isRequired, value: PropTypes.node, diff --git a/src/components/Widgets/Markdown/MarkdownControl/Toolbar/Toolbar.js b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/Toolbar.js index 31087c8c..93d673c4 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/Toolbar/Toolbar.js +++ b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/Toolbar.js @@ -10,14 +10,15 @@ import styles from './Toolbar.css'; export default class Toolbar extends React.Component { static propTypes = { - buttons: PropTypes.object.isRequired, + buttons: PropTypes.object, onToggleMode: PropTypes.func.isRequired, rawMode: PropTypes.bool, plugins: ImmutablePropTypes.map, - onSubmit: PropTypes.func.isRequired, - onAddAsset: PropTypes.func.isRequired, - onRemoveAsset: PropTypes.func.isRequired, - getAsset: PropTypes.func.isRequired, + onSubmit: PropTypes.func, + onAddAsset: PropTypes.func, + onRemoveAsset: PropTypes.func, + getAsset: PropTypes.func, + disabled: PropTypes.bool, }; constructor(props) { @@ -42,15 +43,17 @@ export default class Toolbar extends React.Component { render() { const { - buttons, onToggleMode, rawMode, plugins, onAddAsset, onRemoveAsset, getAsset, + disabled, } = this.props; + const buttons = this.props.buttons || {}; + const { activePlugin } = this.state; const buttonsConfig = [ @@ -64,11 +67,18 @@ export default class Toolbar extends React.Component { return ( <div className={styles.Toolbar}> { buttonsConfig.map((btn, i) => ( - <ToolbarButton key={i} action={btn.state.onAction} active={btn.state.active} {...btn}/> + <ToolbarButton + key={i} + action={btn.state && btn.state.onAction || (() => {})} + active={btn.state && btn.state.active} + disabled={disabled} + {...btn} + /> ))} <ToolbarComponentsMenu plugins={plugins} onComponentMenuItemClick={this.handlePluginFormDisplay} + disabled={disabled} /> {activePlugin && <ToolbarPluginForm diff --git a/src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarButton.css b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarButton.css index f09406df..201c0ee8 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarButton.css +++ b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarButton.css @@ -5,7 +5,10 @@ padding: 6px; border: none; background-color: transparent; - cursor: pointer; + + &:not(:disabled) { + cursor: pointer; + } } .active { diff --git a/src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarButton.js b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarButton.js index 3a276ed8..3feeba24 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarButton.js +++ b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarButton.js @@ -3,11 +3,12 @@ import classnames from 'classnames'; import { Icon } from '../../../../UI'; import styles from './ToolbarButton.css'; -const ToolbarButton = ({ label, icon, action, active }) => ( +const ToolbarButton = ({ label, icon, action, active, disabled }) => ( <button className={classnames(styles.button, { [styles.active]: active })} onClick={action} title={label} + disabled={disabled} > { icon ? <Icon type={icon} /> : label } </button> diff --git a/src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarComponentsMenu.js b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarComponentsMenu.js index ba1e22ea..e6bf3a2d 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarComponentsMenu.js +++ b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarComponentsMenu.js @@ -6,7 +6,7 @@ import styles from './ToolbarComponentsMenu.css'; export default class ToolbarComponentsMenu extends React.Component { static PropTypes = { - plugins: ImmutablePropTypes.map.isRequired, + plugins: ImmutablePropTypes.map, onComponentMenuItemClick: PropTypes.func.isRequired, }; @@ -26,17 +26,22 @@ export default class ToolbarComponentsMenu extends React.Component { }; render() { - const { plugins, onComponentMenuItemClick } = this.props; + const { plugins, onComponentMenuItemClick, disabled } = this.props; return ( <div className={styles.root}> - <ToolbarButton label="Add Component" icon="plus" action={this.handleComponentsMenuToggle}/> + <ToolbarButton + label="Add Component" + icon="plus" + action={this.handleComponentsMenuToggle} + disabled={disabled} + /> <Menu active={this.state.componentsMenuActive} position="auto" onHide={this.handleComponentsMenuHide} ripple={false} > - {plugins.map(plugin => ( + {plugins && plugins.map(plugin => ( <MenuItem key={plugin.get('id')} value={plugin.get('id')}