armo-editor
Version:
React text editor component.
174 lines (145 loc) • 4.59 kB
JavaScript
// Based on these two files:
// https://github.com/JedWatson/react-codemirror/blob/master/src/Codemirror.js
// https://github.com/FormidableLabs/component-playground/blob/master/src/components/editor.jsx
import styles from './Editor.less'
import ExecutionEnvironment from 'exenv'
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import cx from 'classnames'
import debounce from 'lodash.debounce'
import createPrefixer from 'utils/createPrefixer'
let codeMirror
if (ExecutionEnvironment.canUseDOM) {
codeMirror = require('codemirror')
require("codemirror/mode/jsx/jsx")
require("codemirror/mode/css/css")
require("codemirror/mode/markdown/markdown")
}
const prefix = createPrefixer('controls', 'Editor')
const OPTION_PROPS = [
'readOnly',
'lineNumbers',
'lineWrapping',
'mode',
'theme',
]
function normalizeLineEndings (str) {
if (!str) return str;
return str.replace(/\r\n|\r/g, '\n');
}
.withName('Editor')
export default class Editor extends Component {
static propTypes = {
theme: PropTypes.string,
readOnly: PropTypes.bool,
fitToContent: PropTypes.bool,
value: PropTypes.string,
selectedLines: PropTypes.array,
onChange: PropTypes.func,
mode: PropTypes.oneOf(['jsx', 'css']),
lineNumbers: PropTypes.bool,
lineWrapping: PropTypes.bool,
style: PropTypes.object,
className: PropTypes.string,
}
static defaultProps = {
theme: "monokai",
fitToContent: false,
}
state = {
isFocused: false,
}
constructor(props) {
super(props)
this.componentWillReceiveProps = debounce(this.componentWillReceiveProps, 0)
}
componentDidMount() {
const options = {
matchBrackets: true,
smartIndent: false,
tabSize: 2,
indentWithTabs: false,
extraKeys: {
Tab: function(cm) {
var spaces = Array(cm.getOption("indentUnit") + 1).join(" ");
cm.replaceSelection(spaces);
}
},
}
for (let key of OPTION_PROPS) {
options[key] = this.props[key]
}
if (this.props.fitToContent) {
options.viewportMargin = Infinity
}
this.codeMirror = codeMirror.fromTextArea(this.textareaNode, options);
this.codeMirror.on('change', this.handleChange);
this.codeMirror.on('focus', this.handleFocus.bind(this, true));
this.codeMirror.on('blur', this.handleFocus.bind(this, false));
this.codeMirror.on('scroll', this.handleScroll);
this.codeMirror.on('scrollCursorIntoView', this.handleScrollIntoView);
this.codeMirror.setValue(this.props.defaultValue || this.props.value || '');
}
componentWillReceiveProps(nextProps) {
if (this.codeMirror && nextProps.value !== undefined && normalizeLineEndings(this.codeMirror.getValue()) !== normalizeLineEndings(nextProps.value)) {
if (this.props.preserveScrollPosition) {
var prevScrollPosition = this.codeMirror.getScrollInfo();
this.codeMirror.setValue(nextProps.value);
this.codeMirror.scrollTo(prevScrollPosition.left, prevScrollPosition.top);
} else {
this.codeMirror.setValue(nextProps.value);
}
}
for (let key of OPTION_PROPS) {
const prop = nextProps[key]
if (prop !== this.props[key]) {
this.codeMirror.setOption(key, prop)
}
}
}
componentWillUnmount() {
// is there a lighter-weight way to remove the cm instance?
if (this.codeMirror) {
this.codeMirror.toTextArea();
}
}
highlightSelectedLines = () => {
if (Array.isArray(this.props.selectedLines)) {
this.props.selectedLines.forEach(lineNumber =>
this.codeMirror.addLineClass(lineNumber, "wrap", "CodeMirror-activeline-background"))
}
}
focus() {
if (this.codeMirror) {
this.codeMirror.focus()
}
}
render() {
const { focused } = this.state
return (
<div className={cx({ focused })}>
<textarea
ref={this.receiveTextareaRef}
defaultValue={this.props.value}
autoComplete="off"
/>
</div>
)
}
receiveTextareaRef = (ref) => this.textareaNode = ref
handleFocus(isFocused) {
this.setState({ isFocused })
}
handleChange = (doc, change) => {
if (!this.props.readOnly && this.props.onChange && change.origin !== 'setValue') {
this.props.onChange(doc.getValue())
}
}
handleScroll = (codeMirror) => {
this.props.onScroll && this.props.onScroll(codeMirror.getScrollInfo())
}
handleScrollIntoView = (codeMirror, e) => {
e.preventDefault()
}
}