From b390c19bcb8c7d2eafc7c758bb206a41f3d611d5 Mon Sep 17 00:00:00 2001 From: James Hrisho Date: Mon, 22 May 2017 17:38:31 -0400 Subject: [PATCH 01/10] Split Editor Component - Adds support for the split editor component. - Allows for the export of multiple child components in addition to ReactAce being default --- example/index.html | 2 +- example/index.js | 22 +- example/split.html | 17 ++ example/split.js | 286 ++++++++++++++++++++++++++ package.json | 13 +- src/ace.jsx | 25 +-- src/editorOptions.js | 27 +++ src/index.js | 6 + src/split.jsx | 400 +++++++++++++++++++++++++++++++++++ tests/src/split.spec.js | 423 ++++++++++++++++++++++++++++++++++++++ webpack.config.example.js | 11 +- 11 files changed, 1194 insertions(+), 38 deletions(-) create mode 100644 example/split.html create mode 100644 example/split.js create mode 100644 src/editorOptions.js create mode 100644 src/index.js create mode 100644 src/split.jsx create mode 100644 tests/src/split.spec.js diff --git a/example/index.html b/example/index.html index fb3830ee..2259f389 100644 --- a/example/index.html +++ b/example/index.html @@ -12,6 +12,6 @@

React-Ace

- + diff --git a/example/index.js b/example/index.js index b1e9250a..3738437b 100644 --- a/example/index.js +++ b/example/index.js @@ -1,12 +1,8 @@ import React, { Component } from 'react'; import { render } from 'react-dom'; import AceEditor from '../src/ace.jsx'; -import brace from 'brace'; - - import 'brace/mode/jsx'; - const languages = [ 'javascript', 'java', @@ -223,6 +219,24 @@ class App extends Component { showPrintMargin={this.state.showPrintMargin} showGutter={this.state.showGutter} highlightActiveLine={this.state.highlightActiveLine} + commands={[ + { + name: 'myReactAceTest', + bindKey: {win: 'Ctrl-M', mac: 'Command-M'}, + exec: () => { + console.log("this coammdb or whatever") + }, + readOnly: true + }, + { + name: 'myTestCommand', + bindKey: {win: 'Ctrl-W', mac: 'Command-W'}, + exec: () => { + console.log("this coammdb or whatever") + }, + readOnly: true + } + ]} value={this.state.value} setOptions={{ enableBasicAutocompletion: this.state.enableBasicAutocompletion, diff --git a/example/split.html b/example/split.html new file mode 100644 index 00000000..3bfdef2c --- /dev/null +++ b/example/split.html @@ -0,0 +1,17 @@ + + + + + Split Editor + + + +
+
+

React-Ace: Split Editor Example

+
+
+
+ + + diff --git a/example/split.js b/example/split.js new file mode 100644 index 00000000..796792bb --- /dev/null +++ b/example/split.js @@ -0,0 +1,286 @@ +import React, { Component } from 'react'; +import { render } from 'react-dom'; +import SplitAceEditor from '../src/split.jsx'; + +import 'brace/mode/jsx'; + + +const languages = [ + 'javascript', + 'java', + 'python', + 'xml', + 'ruby', + 'sass', + 'markdown', + 'mysql', + 'json', + 'html', + 'handlebars', + 'golang', + 'csharp', + 'elixir', + 'typescript', + 'css' +] + +const themes = [ + 'monokai', + 'github', + 'tomorrow', + 'kuroir', + 'twilight', + 'xcode', + 'textmate', + 'solarized_dark', + 'solarized_light', + 'terminal', +] + +languages.forEach((lang) => { + require(`brace/mode/${lang}`) +}) + +themes.forEach((theme) => { + require(`brace/theme/${theme}`) +}) +/*eslint-disable no-alert, no-console */ +import 'brace/ext/language_tools'; + + +const defaultValue = [ + `function onLoad(editor) { + console.log(\"i\'ve loaded\"); + }`, + 'const secondInput = "me i am the second input";' +]; +class App extends Component { + onLoad() { + console.log('i\'ve loaded'); + } + onChange(newValue) { + console.log('change', newValue); + this.setState({ + value: newValue + }) + } + + onSelectionChange(newValue, event) { + console.log('select-change', newValue); + console.log('select-change-event', event); + } + setTheme(e) { + this.setState({ + theme: e.target.value + }) + } + setMode(e) { + this.setState({ + mode: e.target.value + }) + } + setBoolean(name, value) { + this.setState({ + [name]: value + }) + } + setFontSize(e) { + this.setState({ + fontSize: parseInt(e.target.value,10) + }) + } + setSplits(e) { + this.setState({ + splits: parseInt(e.target.value,10) + }) + } + setOrientation(e) { + this.setState({ + orientation: e.target.value + }) + } + constructor(props) { + super(props); + this.state = { + splits: 2, + orientation: 'beside', + value: defaultValue, + theme: 'github', + mode: 'javascript', + enableBasicAutocompletion: false, + enableLiveAutocompletion: false, + fontSize: 14, + showGutter: true, + showPrintMargin: true, + highlightActiveLine: true, + enableSnippets: false, + showLineNumbers: true, + }; + this.setTheme = this.setTheme.bind(this); + this.setMode = this.setMode.bind(this); + this.onChange = this.onChange.bind(this); + this.setFontSize = this.setFontSize.bind(this); + this.setBoolean = this.setBoolean.bind(this); + this.setSplits = this.setSplits.bind(this); + this.setOrientation = this.setOrientation.bind(this); + } + render() { + return ( +
+
+
+ +

+ + + +

+
+ +
+ +

+ + +

+
+ +
+ +

+ + +

+
+ +
+ +

+ + +

+
+ +
+ +

+ + +

+
+
+

+ +

+
+
+

+ +

+
+
+

+ +

+
+
+

+ +

+
+
+

+ +

+
+
+

+ +

+
+
+

+ +

+
+ + +
+
+

Editor

+ +
+
+ ); + } +} + + +render( + , + document.getElementById('example') +); \ No newline at end of file diff --git a/package.json b/package.json index 86b06524..88383c28 100644 --- a/package.json +++ b/package.json @@ -2,14 +2,14 @@ "name": "react-ace", "version": "4.4.0", "description": "A react component for Ace Editor", - "main": "lib/ace.js", + "main": "lib/index.js", "types": "types.d.ts", "scripts": { "clean": "rimraf lib dist", - "lint": "node_modules/.bin/eslint src/ace.jsx", + "lint": "node_modules/.bin/eslint src/*", "build:lib": "babel src --out-dir lib", - "build:umd": "webpack src/ace.jsx dist/react-ace.js --config webpack.config.development.js", - "build:umd:min": "webpack src/ace.jsx dist/react-ace.min.js --config webpack.config.production.js", + "build:umd": "webpack src/index.js dist/react-ace.js --config webpack.config.development.js", + "build:umd:min": "webpack src/index.js dist/react-ace.min.js --config webpack.config.production.js", "example": "webpack-dev-server --config webpack.config.example.js", "build:example": "webpack --config webpack.config.example.js", "build": "npm run build:lib && npm run build:umd && npm run build:umd:min", @@ -25,7 +25,8 @@ "babel": { "presets": [ "es2015", - "react" + "react", + "stage-2" ], "plugins": [ "transform-object-rest-spread" @@ -41,6 +42,7 @@ "babel-plugin-transform-object-rest-spread": "^6.23.0", "babel-preset-es2015": "^6.24.1", "babel-preset-react": "^6.24.1", + "babel-preset-stage-2": "^6.24.1", "chai": "^3.5.0", "coveralls": "^2.13.1", "enzyme": "^2.4.1", @@ -66,6 +68,7 @@ ], "dependencies": { "brace": "^0.10.0", + "lodash.get": "^4.4.2", "lodash.isequal": "^4.1.1", "opencollective": "^1.0.3", "prop-types": "^15.5.8" diff --git a/src/ace.jsx b/src/ace.jsx index 7b4dc486..19631ea2 100644 --- a/src/ace.jsx +++ b/src/ace.jsx @@ -4,33 +4,12 @@ import PropTypes from 'prop-types' import isEqual from 'lodash.isequal' const { Range } = ace.acequire('ace/range'); - -const editorOptions = [ - 'minLines', - 'maxLines', - 'readOnly', - 'highlightActiveLine', - 'tabSize', - 'enableBasicAutocompletion', - 'enableLiveAutocompletion', - 'enableSnippets', -]; +import { editorOptions, editorEvents } from './editorOptions.js' export default class ReactAce extends Component { constructor(props) { super(props); - [ - 'onChange', - 'onFocus', - 'onBlur', - 'onCopy', - 'onPaste', - 'onSelectionChange', - 'onScroll', - 'handleOptions', - 'updateRef', - ] - .forEach(method => { + editorEvents.forEach(method => { this[method] = this[method].bind(this); }); } diff --git a/src/editorOptions.js b/src/editorOptions.js new file mode 100644 index 00000000..6a7ddbdb --- /dev/null +++ b/src/editorOptions.js @@ -0,0 +1,27 @@ +const editorOptions = [ + 'minLines', + 'maxLines', + 'readOnly', + 'highlightActiveLine', + 'tabSize', + 'enableBasicAutocompletion', + 'enableLiveAutocompletion', + 'enableSnippets', +] + +const editorEvents = [ + 'onChange', + 'onFocus', + 'onBlur', + 'onCopy', + 'onPaste', + 'onSelectionChange', + 'onScroll', + 'handleOptions', + 'updateRef', +] + +export { + editorOptions, + editorEvents +} \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100644 index 00000000..bc5b1647 --- /dev/null +++ b/src/index.js @@ -0,0 +1,6 @@ +import ace from './ace.jsx' +import split from './split.jsx'; +export { + split +} +export default ace \ No newline at end of file diff --git a/src/split.jsx b/src/split.jsx new file mode 100644 index 00000000..f815ec78 --- /dev/null +++ b/src/split.jsx @@ -0,0 +1,400 @@ +import ace from 'brace' +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import isEqual from 'lodash.isequal' +import get from 'lodash.get' + +import { editorOptions, editorEvents } from './editorOptions.js' +const { Range } = ace.acequire('ace/range'); + +import 'brace/ext/split' +const { Split } = ace.acequire('ace/split'); + +export default class SplitComponent extends Component { + constructor(props) { + super(props); + editorEvents.forEach(method => { + this[method] = this[method].bind(this); + }); + } + + componentDidMount() { + const { + className, + onBeforeLoad, + mode, + focus, + theme, + fontSize, + value, + defaultValue, + cursorStart, + showGutter, + wrapEnabled, + showPrintMargin, + scrollMargin = [ 0, 0, 0, 0], + keyboardHandler, + onLoad, + commands, + annotations, + markers, + splits, + } = this.props; + + this.editor = ace.edit(this.refEditor); + + if (onBeforeLoad) { + onBeforeLoad(ace); + } + + const editorProps = Object.keys(this.props.editorProps); + + var split = new Split(this.editor.container,`ace/theme/${theme}`,splits) + this.editor.env.split = split; + + this.splitEditor = split.getEditor(0); + this.split = split + // in a split scenario we don't want a print margin for the entire application + this.editor.setShowPrintMargin(false); + this.editor.renderer.setShowGutter(false); + // get a list of possible options to avoid 'misspelled option errors' + const availableOptions = this.splitEditor.$options; + split.forEach((editor, index) => { + for (let i = 0; i < editorProps.length; i++) { + editor[editorProps[i]] = this.props.editorProps[editorProps[i]]; + } + const defaultValueForEditor = get(defaultValue, index) + const valueForEditor = get(value, index, '') + editor.setTheme(`ace/theme/${theme}`); + editor.renderer.setScrollMargin(scrollMargin[0], scrollMargin[1], scrollMargin[2], scrollMargin[3]) + editor.getSession().setMode(`ace/mode/${mode}`); + editor.setFontSize(fontSize); + editor.renderer.setShowGutter(showGutter); + editor.getSession().setUseWrapMode(wrapEnabled); + editor.setShowPrintMargin(showPrintMargin); + editor.on('focus', this.onFocus); + editor.on('blur', this.onBlur); + editor.on('copy', this.onCopy); + editor.on('paste', this.onPaste); + editor.on('change', this.onChange); + editor.getSession().selection.on('changeSelection', this.onSelectionChange); + editor.session.on('changeScrollTop', this.onScroll); + editor.setValue(defaultValueForEditor === undefined ? valueForEditor : defaultValueForEditor, cursorStart); + const newAnnotations = get(annotations, index, []) + const newMarkers = get(markers, index, []) + editor.getSession().setAnnotations(newAnnotations); + if(newMarkers && newMarkers.length > 0){ + this.handleMarkers(newMarkers,editor); + } + + for (let i = 0; i < editorOptions.length; i++) { + const option = editorOptions[i]; + if (availableOptions.hasOwnProperty(option)) { + editor.setOption(option, this.props[option]); + } + } + this.handleOptions(this.props, editor); + if (Array.isArray(commands)) { + commands.forEach((command) => { + editor.commands.addCommand(command); + }); + } + + if (keyboardHandler) { + editor.setKeyboardHandler('ace/keyboard/' + keyboardHandler); + } + }) + + if (className) { + this.refEditor.className += ' ' + className; + } + + if (focus) { + this.splitEditor.focus(); + } + + const sp = this.editor.env.split; + sp.setOrientation( this.props.orientation === 'below' ? sp.BELOW : sp.BESIDE); + sp.resize(true) + if (onLoad) { + onLoad(this.editor); + } + } + + componentWillReceiveProps(nextProps) { + const oldProps = this.props; + + const split = this.editor.env.split + + if (nextProps.splits !== oldProps.splits) { + split.setSplits(nextProps.splits) + } + + if (nextProps.orientation !== oldProps.orientation) { + split.setOrientation( nextProps.orientation === 'below' ? split.BELOW : split.BESIDE); + } + + split.forEach((editor, index) => { + + if (nextProps.mode !== oldProps.mode) { + editor.getSession().setMode('ace/mode/' + nextProps.mode); + } + if (nextProps.keyboardHandler !== oldProps.keyboardHandler) { + if (nextProps.keyboardHandler) { + editor.setKeyboardHandler('ace/keyboard/' + nextProps.keyboardHandler); + } else { + editor.setKeyboardHandler(null); + } + } + if (nextProps.fontSize !== oldProps.fontSize) { + editor.setFontSize(nextProps.fontSize); + } + if (nextProps.wrapEnabled !== oldProps.wrapEnabled) { + editor.getSession().setUseWrapMode(nextProps.wrapEnabled); + } + if (nextProps.showPrintMargin !== oldProps.showPrintMargin) { + editor.setShowPrintMargin(nextProps.showPrintMargin); + } + if (nextProps.showGutter !== oldProps.showGutter) { + editor.renderer.setShowGutter(nextProps.showGutter); + } + + for (let i = 0; i < editorOptions.length; i++) { + const option = editorOptions[i]; + if (nextProps[option] !== oldProps[option]) { + editor.setOption(option, nextProps[option]); + } + } + if (!isEqual(nextProps.setOptions, oldProps.setOptions)) { + this.handleOptions(nextProps, editor); + } + const nextValue = get(nextProps.value, index, '') + if (editor.getValue() !== nextValue) { + // editor.setValue is a synchronous function call, change event is emitted before setValue return. + this.silent = true; + const pos = editor.session.selection.toJSON(); + editor.setValue(nextValue, nextProps.cursorStart); + editor.session.selection.fromJSON(pos); + this.silent = false; + } + const newAnnotations = get(nextProps.annotations, index, []) + const oldAnnotations = get(oldProps.annotations, index, []) + if (!isEqual(newAnnotations, oldAnnotations)) { + editor.getSession().setAnnotations(newAnnotations); + } + + const newMarkers = get(nextProps.markers, index, []) + const oldMarkers = get(oldProps.markers, index, []) + if (!isEqual(newMarkers, oldMarkers) && (newMarkers && newMarkers.length > 0)) { + this.handleMarkers(newMarkers, editor); + } + + }) + + if (nextProps.className !== oldProps.className) { + let appliedClasses = this.refEditor.className; + let appliedClassesArray = appliedClasses.trim().split(' '); + let oldClassesArray = oldProps.className.trim().split(' '); + oldClassesArray.forEach((oldClass) => { + let index = appliedClassesArray.indexOf(oldClass); + appliedClassesArray.splice(index, 1); + }); + this.refEditor.className = ' ' + nextProps.className + ' ' + appliedClassesArray.join(' '); + } + + if (nextProps.theme !== oldProps.theme) { + split.setTheme('ace/theme/' + nextProps.theme); + } + + if (nextProps.focus && !oldProps.focus) { + this.splitEditor.focus(); + } + if(nextProps.height !== this.props.height){ + this.editor.resize(); + } + } + + componentWillUnmount() { + this.editor.destroy(); + this.editor = null; + } + + onChange(event) { + if (this.props.onChange && !this.silent) { + let value = [] + this.editor.env.split.forEach((editor) => { + value.push(editor.getValue()) + }) + this.props.onChange(value, event); + } + } + + onSelectionChange(event) { + if (this.props.onSelectionChange) { + let value = [] + this.editor.env.split.forEach((editor) => { + value.push(editor.getSelection()) + }) + this.props.onSelectionChange(value, event); + } + } + + onFocus() { + if (this.props.onFocus) { + this.props.onFocus(); + } + } + + onBlur() { + if (this.props.onBlur) { + this.props.onBlur(); + } + } + + onCopy(text) { + if (this.props.onCopy) { + this.props.onCopy(text); + } + } + + onPaste(text) { + if (this.props.onPaste) { + this.props.onPaste(text); + } + } + + onScroll() { + if (this.props.onScroll) { + this.props.onScroll(this.editor); + } + } + + handleOptions(props, editor) { + const setOptions = Object.keys(props.setOptions); + for (let y = 0; y < setOptions.length; y++) { + editor.setOption(setOptions[y], props.setOptions[setOptions[y]]); + } + } + + handleMarkers(markers, editor) { + // remove foreground markers + let currentMarkers = editor.getSession().getMarkers(true); + for (const i in currentMarkers) { + if (currentMarkers.hasOwnProperty(i)) { + editor.getSession().removeMarker(currentMarkers[i].id); + } + } + // remove background markers + currentMarkers = editor.getSession().getMarkers(false); + for (const i in currentMarkers) { + if (currentMarkers.hasOwnProperty(i)) { + editor.getSession().removeMarker(currentMarkers[i].id); + } + } + // add new markers + markers.forEach(({ startRow, startCol, endRow, endCol, className, type, inFront = false }) => { + const range = new Range(startRow, startCol, endRow, endCol); + editor.getSession().addMarker(range, className, type, inFront); + }); + } + + updateRef(item) { + this.refEditor = item; + } + + render() { + const { name, width, height, style } = this.props; + const divStyle = { width, height, ...style }; + return ( +
+
+ ); + } +} + +SplitComponent.propTypes = { + mode: PropTypes.string, + splits: PropTypes.number, + orientation: PropTypes.string, + focus: PropTypes.bool, + theme: PropTypes.string, + name: PropTypes.string, + className: PropTypes.string, + height: PropTypes.string, + width: PropTypes.string, + fontSize: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]), + showGutter: PropTypes.bool, + onChange: PropTypes.func, + onCopy: PropTypes.func, + onPaste: PropTypes.func, + onFocus: PropTypes.func, + onBlur: PropTypes.func, + onScroll: PropTypes.func, + value: PropTypes.arrayOf(PropTypes.string), + defaultValue: PropTypes.arrayOf(PropTypes.string), + onLoad: PropTypes.func, + onSelectionChange: PropTypes.func, + onBeforeLoad: PropTypes.func, + minLines: PropTypes.number, + maxLines: PropTypes.number, + readOnly: PropTypes.bool, + highlightActiveLine: PropTypes.bool, + tabSize: PropTypes.number, + showPrintMargin: PropTypes.bool, + cursorStart: PropTypes.number, + editorProps: PropTypes.object, + setOptions: PropTypes.object, + style: PropTypes.object, + scrollMargin: PropTypes.array, + annotations: PropTypes.array, + markers: PropTypes.array, + keyboardHandler: PropTypes.string, + wrapEnabled: PropTypes.bool, + enableBasicAutocompletion: PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.array, + ]), + enableLiveAutocompletion: PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.array, + ]), + commands: PropTypes.array, +}; + +SplitComponent.defaultProps = { + name: 'brace-editor', + focus: false, + orientation: 'beside', + splits: 2, + mode: '', + theme: '', + height: '500px', + width: '500px', + value: [], + fontSize: 12, + showGutter: true, + onChange: null, + onPaste: null, + onLoad: null, + onScroll: null, + minLines: null, + maxLines: null, + readOnly: false, + highlightActiveLine: true, + showPrintMargin: true, + tabSize: 4, + cursorStart: 1, + editorProps: {}, + style: {}, + scrollMargin: [ 0, 0, 0, 0], + setOptions: {}, + wrapEnabled: false, + enableBasicAutocompletion: false, + enableLiveAutocompletion: false, +}; \ No newline at end of file diff --git a/tests/src/split.spec.js b/tests/src/split.spec.js new file mode 100644 index 00000000..33a67aa2 --- /dev/null +++ b/tests/src/split.spec.js @@ -0,0 +1,423 @@ +import { expect } from 'chai'; +import React from 'react'; +import sinon from 'sinon'; +import ace from 'brace'; +import { mount } from 'enzyme'; +import SplitEditor from '../../src/split.jsx'; +import brace from 'brace'; // eslint-disable-line no-unused-vars + +describe('Split Component', () => { + + // Required for the document.getElementById used by Ace can work in the test environment + const domElement = document.getElementById('app'); + const mountOptions = { + attachTo: domElement, + }; + + describe('General', () => { + + it('should render without problems with defaults properties', () => { + const wrapper = mount(, mountOptions); + expect(wrapper).to.exist; + }); + it('should get the ace library from the onBeforeLoad callback', () => { + const beforeLoadCallback = sinon.spy(); + mount(, mountOptions); + + expect(beforeLoadCallback.callCount).to.equal(1); + expect(beforeLoadCallback.getCall(0).args[0]).to.deep.equal(ace); + }); + + it('should set the editor props to the Ace element', () => { + const editorProperties = { + react: 'setFromReact', + test: 'setFromTest', + }; + const wrapper = mount(, mountOptions); + + const editor = wrapper.instance().splitEditor; + + expect(editor.react).to.equal(editorProperties.react); + expect(editor.test).to.equal(editorProperties.test); + }); + + it('should update the orientation on componentWillReceiveProps', () => { + let orientation = 'below'; + const wrapper = mount(, mountOptions); + + // Read set value + let editor = wrapper.instance().split; + expect(editor.getOrientation()).to.equal(editor.BELOW); + + // Now trigger the componentWillReceiveProps + orientation = 'beside'; + wrapper.setProps({orientation}); + editor = wrapper.instance().split; + expect(editor.getOrientation()).to.equal(editor.BESIDE); + }); + + it('should update the orientation on componentWillReceiveProps', () => { + const wrapper = mount(, mountOptions); + + // Read set value + let editor = wrapper.instance().split; + expect(editor.getSplits()).to.equal(2); + + // Now trigger the componentWillReceiveProps + wrapper.setProps({splits: 4}); + editor = wrapper.instance().split; + expect(editor.getSplits()).to.equal(4); + }); + + it('should set the command for the Ace element', () => { + const commandsMock = [ + { + name: 'myReactAceTest', + bindKey: {win: 'Ctrl-M', mac: 'Command-M'}, + exec: () => { + }, + readOnly: true + }, + { + name: 'myTestCommand', + bindKey: {win: 'Ctrl-W', mac: 'Command-W'}, + exec: () => { + }, + readOnly: true + } + ]; + const wrapper = mount(, mountOptions); + + const editor = wrapper.instance().splitEditor; + expect(editor.commands.commands.myReactAceTest).to.deep.equal(commandsMock[0]); + expect(editor.commands.commands.myTestCommand).to.deep.equal(commandsMock[1]); + }); + + it('should get the editor from the onLoad callback', () => { + const loadCallback = sinon.spy(); + const wrapper = mount(, mountOptions); + + // Get the editor + const editor = wrapper.instance().editor; + + expect(loadCallback.callCount).to.equal(1); + expect(loadCallback.getCall(0).args[0]).to.deep.equal(editor); + }); + + it('should trigger the focus on mount', () => { + const onFocusCallback = sinon.spy(); + mount(, mountOptions); + + // Read the focus + expect(onFocusCallback.callCount).to.equal(1); + }); + + + it('should set editor to null on componentWillUnmount', () => { + const wrapper = mount(, mountOptions); + expect(wrapper.node.editor).to.not.equal(null); + + // Check the editor is null after the Unmount + wrapper.unmount(); + expect(wrapper.node.editor).to.equal(null); + }); + + + + }); + + describe('Events', () => { + + it('should call the onChange method callback', () => { + const onChangeCallback = sinon.spy(); + const wrapper = mount(, mountOptions); + + // Check is not previously called + expect(onChangeCallback.callCount).to.equal(0); + + // Trigger the change event + const expectText = 'React Ace Test'; + wrapper.instance().splitEditor.setValue(expectText, 1); + + expect(onChangeCallback.callCount).to.equal(1); + expect(onChangeCallback.getCall(0).args[0]).to.deep.equal([expectText, '']); + expect(onChangeCallback.getCall(0).args[1].action).to.eq('insert') + }); + + it('should call the onCopy method', () => { + const onCopyCallback = sinon.spy(); + const wrapper = mount(, mountOptions); + + // Check is not previously called + expect(onCopyCallback.callCount).to.equal(0); + + // Trigger the copy event + const expectText = 'React Ace Test'; + wrapper.instance().onCopy(expectText); + + expect(onCopyCallback.callCount).to.equal(1); + expect(onCopyCallback.getCall(0).args[0]).to.equal(expectText); + }); + + it('should call the onPaste method', () => { + const onPasteCallback = sinon.spy(); + const wrapper = mount(, mountOptions); + + // Check is not previously called + expect(onPasteCallback.callCount).to.equal(0); + + // Trigger the Paste event + const expectText = 'React Ace Test'; + wrapper.instance().onPaste(expectText); + + expect(onPasteCallback.callCount).to.equal(1); + expect(onPasteCallback.getCall(0).args[0]).to.equal(expectText); + }); + + it('should call the onFocus method callback', () => { + const onFocusCallback = sinon.spy(); + const wrapper = mount(, mountOptions); + + // Check is not previously called + expect(onFocusCallback.callCount).to.equal(0); + + // Trigger the focus event + wrapper.instance().split.focus(); + + expect(onFocusCallback.callCount).to.equal(1); + }); + + it('should call the onSelectionChange method callback', () => { + const onSelectionChangeCallback = sinon.spy(); + const wrapper = mount(, mountOptions); + + // Check is not previously called + expect(onSelectionChangeCallback.callCount).to.equal(0); + + // Trigger the focus event + wrapper.instance().splitEditor.getSession().selection.selectAll() + + expect(onSelectionChangeCallback.callCount).to.equal(1); + }); + + it('should call the onBlur method callback', () => { + const onBlurCallback = sinon.spy(); + const wrapper = mount(, mountOptions); + + // Check is not previously called + expect(onBlurCallback.callCount).to.equal(0); + + // Trigger the blur event + wrapper.instance().onBlur(); + + expect(onBlurCallback.callCount).to.equal(1); + }); + + it('should not trigger a component error to call the events without setting the props', () => { + const wrapper = mount(, mountOptions); + + // Check the if statement is checking if the property is set. + wrapper.instance().onChange(); + wrapper.instance().onCopy('copy'); + wrapper.instance().onPaste('paste'); + wrapper.instance().onFocus(); + wrapper.instance().onBlur(); + }); + + }); + describe('ComponentWillReceiveProps', () => { + + it('should update the editorOptions on componentWillReceiveProps', () => { + const options = { + printMargin: 80 + }; + const wrapper = mount(, mountOptions); + + // Read set value + const editor = wrapper.instance().splitEditor; + expect(editor.getOption('printMargin')).to.equal(options.printMargin); + + // Now trigger the componentWillReceiveProps + const newOptions = { + printMargin: 200, + animatedScroll: true, + }; + wrapper.setProps({setOptions: newOptions}); + expect(editor.getOption('printMargin')).to.equal(newOptions.printMargin); + expect(editor.getOption('animatedScroll')).to.equal(newOptions.animatedScroll); + }); + it('should update the editorOptions on componentWillReceiveProps', () => { + + const wrapper = mount(, mountOptions); + + // Read set value + const editor = wrapper.instance().splitEditor; + expect(editor.getOption('minLines')).to.equal(1); + + + wrapper.setProps({minLines: 2}); + expect(editor.getOption('minLines')).to.equal(2); + }); + + + it('should update the mode on componentWillReceiveProps', () => { + + const wrapper = mount(, mountOptions); + + // Read set value + const oldMode = wrapper.first('SplitEditor').props() + + wrapper.setProps({mode: 'elixir'}); + const newMode = wrapper.first('SplitEditor').props() + expect(oldMode).to.not.deep.equal(newMode); + }); + + + + + it('should update many props on componentWillReceiveProps', () => { + + const wrapper = mount(( + ), mountOptions); + + // Read set value + const oldMode = wrapper.first('SplitEditor').props() + + wrapper.setProps({ + theme: 'solarized', + keyboardHandler: 'emacs', + fontSize: 18, + wrapEnabled: false, + showPrintMargin: false, + showGutter: true, + height: '120px', + }); + const newMode = wrapper.first('SplitEditor').props() + expect(oldMode).to.not.deep.equal(newMode); + }); + + + + it('should update the className on componentWillReceiveProps', () => { + const className = 'old-class'; + const wrapper = mount(, mountOptions); + + // Read set value + let editor = wrapper.node.refEditor; + expect(editor.className).to.equal(' ace_editor ace-tm old-class'); + + // Now trigger the componentWillReceiveProps + const newClassName = 'new-class'; + wrapper.setProps({className: newClassName}); + editor = wrapper.node.refEditor; + expect(editor.className).to.equal(' new-class ace_editor ace-tm'); + }); + + + it('should update the value on componentWillReceiveProps', () => { + const startValue = 'start value'; + const anotherStartValue = 'another start value'; + const wrapper = mount(, mountOptions); + + // Read set value + let editor = wrapper.instance().split.getEditor(0); + let editor2 = wrapper.instance().split.getEditor(1); + expect(editor.getValue()).to.equal(startValue); + expect(editor2.getValue()).to.equal(anotherStartValue); + + // Now trigger the componentWillReceiveProps + const newValue = 'updated value'; + const anotherNewValue = 'another updated value'; + wrapper.setProps({value: [newValue, anotherNewValue]}); + editor = wrapper.instance().splitEditor; + editor2 = wrapper.instance().split.getEditor(1); + expect(editor.getValue()).to.equal(newValue); + expect(editor2.getValue()).to.equal(anotherNewValue); + }); + it('should set up the markers', () => { + const markers = [[{ + startRow: 3, + type: 'text', + className: 'test-marker' + }]]; + const wrapper = mount(, mountOptions); + + // Read the markers + const editor = wrapper.instance().splitEditor; + expect(editor.getSession().getMarkers()['3'].clazz).to.equal('test-marker'); + expect(editor.getSession().getMarkers()['3'].type).to.equal('text'); + }); + + it('should update the markers', () => { + const oldMarkers = [[ + { + startRow: 4, + type: 'text', + className: 'test-marker-old' + }, + { + startRow: 7, + type: 'foo', + className: 'test-marker-old', + inFront: true + } + ]]; + const markers = [[{ + startRow: 3, + type: 'text', + className: 'test-marker-new', + inFront: true, + },{ + startRow: 5, + type: 'text', + className: 'test-marker-new' + }]]; + const wrapper = mount(, mountOptions); + + // Read the markers + const editor = wrapper.instance().splitEditor; + expect(editor.getSession().getMarkers()['3'].clazz).to.equal('test-marker-old'); + expect(editor.getSession().getMarkers()['3'].type).to.equal('text'); + wrapper.setProps({markers: markers}); + const editorB = wrapper.instance().splitEditor; + expect(editorB.getSession().getMarkers()['6'].clazz).to.equal('test-marker-new'); + expect(editorB.getSession().getMarkers()['6'].type).to.equal('text'); + }); + + it('should add annotations', () => { + const annotations = [{ + row: 3, // must be 0 based + column: 4, // must be 0 based + text: 'error.message', // text to show in tooltip + type: 'error' + }] + const wrapper = mount(, mountOptions); + const editor = wrapper.instance().splitEditor; + wrapper.setProps({annotations: [annotations]}); + expect(editor.getSession().getAnnotations()).to.deep.equal(annotations); + wrapper.setProps({annotations: null}); + expect(editor.getSession().getAnnotations()).to.deep.equal([]); + }) + + it('should trigger the focus on componentWillReceiveProps', () => { + const onFocusCallback = sinon.spy(); + const wrapper = mount(, mountOptions); + + // Read the focus + expect(onFocusCallback.callCount).to.equal(0); + + // Now trigger the componentWillReceiveProps + wrapper.setProps({focus: true}); + expect(onFocusCallback.callCount).to.equal(1); + }); + + }); +}); \ No newline at end of file diff --git a/webpack.config.example.js b/webpack.config.example.js index ae69b22c..a2eb929d 100644 --- a/webpack.config.example.js +++ b/webpack.config.example.js @@ -4,12 +4,13 @@ const path = require('path'); module.exports = { devtool: 'source-map', - entry: [ - './example/index', - ], + entry: { + 'index': './example/index', + 'split': './example/split', + }, output: { path: path.join(__dirname, 'example/static'), - filename: 'bundle.js', + filename: '[name].js', publicPath: '/static/', }, plugins: [ @@ -25,7 +26,7 @@ module.exports = { }, devServer: { hot: true, - contentBase: path.join(__dirname, 'example'), + contentBase: [path.join(__dirname, 'example'), path.join(__dirname, 'dist')], compress: true, port: 9000, }, From 6d804af61be12e400cd30c5239a5434cfac9cf9b Mon Sep 17 00:00:00 2001 From: James Hrisho Date: Thu, 25 May 2017 23:27:23 -0400 Subject: [PATCH 02/10] Remove commands from the example --- example/index.js | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/example/index.js b/example/index.js index 3738437b..d9cd5df7 100644 --- a/example/index.js +++ b/example/index.js @@ -219,24 +219,6 @@ class App extends Component { showPrintMargin={this.state.showPrintMargin} showGutter={this.state.showGutter} highlightActiveLine={this.state.highlightActiveLine} - commands={[ - { - name: 'myReactAceTest', - bindKey: {win: 'Ctrl-M', mac: 'Command-M'}, - exec: () => { - console.log("this coammdb or whatever") - }, - readOnly: true - }, - { - name: 'myTestCommand', - bindKey: {win: 'Ctrl-W', mac: 'Command-W'}, - exec: () => { - console.log("this coammdb or whatever") - }, - readOnly: true - } - ]} value={this.state.value} setOptions={{ enableBasicAutocompletion: this.state.enableBasicAutocompletion, From 986bd7018bdb4d948bd0155f9ba9274af04705cc Mon Sep 17 00:00:00 2001 From: James Hrisho Date: Sat, 27 May 2017 10:20:29 -0400 Subject: [PATCH 03/10] Add resize to split editor --- src/split.jsx | 2 +- tests/src/split.spec.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/split.jsx b/src/split.jsx index f815ec78..c7f2ae7a 100644 --- a/src/split.jsx +++ b/src/split.jsx @@ -209,7 +209,7 @@ export default class SplitComponent extends Component { if (nextProps.focus && !oldProps.focus) { this.splitEditor.focus(); } - if(nextProps.height !== this.props.height){ + if(nextProps.height !== this.props.height || nextProps.width !== this.props.width){ this.editor.resize(); } } diff --git a/tests/src/split.spec.js b/tests/src/split.spec.js index 33a67aa2..4a47e5fc 100644 --- a/tests/src/split.spec.js +++ b/tests/src/split.spec.js @@ -286,6 +286,7 @@ describe('Split Component', () => { showPrintMargin={true} showGutter={false} height="100px" + width="200px" />), mountOptions); // Read set value @@ -299,6 +300,7 @@ describe('Split Component', () => { showPrintMargin: false, showGutter: true, height: '120px', + width: '220px', }); const newMode = wrapper.first('SplitEditor').props() expect(oldMode).to.not.deep.equal(newMode); From 3ce5144dc0c345d07da568e99b766ba33af6196e Mon Sep 17 00:00:00 2001 From: James Hrisho Date: Sat, 27 May 2017 20:56:07 -0400 Subject: [PATCH 04/10] Better handling of missing imports and mispelled editor options --- example/index.js | 1 + example/split.js | 1 + package.json | 4 ++-- src/ace.jsx | 2 ++ src/split.jsx | 2 ++ tests/src/ace.spec.js | 10 +++++++++- tests/src/split.spec.js | 10 +++++++++- 7 files changed, 26 insertions(+), 4 deletions(-) diff --git a/example/index.js b/example/index.js index d9cd5df7..fe6c9872 100644 --- a/example/index.js +++ b/example/index.js @@ -37,6 +37,7 @@ const themes = [ languages.forEach((lang) => { require(`brace/mode/${lang}`) + require(`brace/snippets/${lang}`) }) themes.forEach((theme) => { diff --git a/example/split.js b/example/split.js index 796792bb..a71b80ce 100644 --- a/example/split.js +++ b/example/split.js @@ -39,6 +39,7 @@ const themes = [ languages.forEach((lang) => { require(`brace/mode/${lang}`) + require(`brace/snippets/${lang}`) }) themes.forEach((theme) => { diff --git a/package.json b/package.json index 88383c28..ea1eb6b7 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "nyc": "^10.3.2", "react-addons-test-utils": "^15.5.1", "rimraf": "^2.5.2", - "sinon": "^2.2.0", + "sinon": "^2.3.2", "webpack": "^2.5.1", "webpack-dev-server": "^2.4.5" }, @@ -103,4 +103,4 @@ "url": "https://opencollective.com/react-ace", "logo": "https://opencollective.com/opencollective/logo.txt" } -} \ No newline at end of file +} diff --git a/src/ace.jsx b/src/ace.jsx index 19631ea2..a1789159 100644 --- a/src/ace.jsx +++ b/src/ace.jsx @@ -74,6 +74,8 @@ export default class ReactAce extends Component { const option = editorOptions[i]; if (availableOptions.hasOwnProperty(option)) { this.editor.setOption(option, this.props[option]); + } else if (this.props[option]) { + console.warn(`ReaceAce: editor option ${option} was activated but not found. Did you need to import a related tool or did you possibly mispell the option?`) } } diff --git a/src/split.jsx b/src/split.jsx index c7f2ae7a..4da872cb 100644 --- a/src/split.jsx +++ b/src/split.jsx @@ -91,6 +91,8 @@ export default class SplitComponent extends Component { const option = editorOptions[i]; if (availableOptions.hasOwnProperty(option)) { editor.setOption(option, this.props[option]); + } else if (this.props[option]) { + console.warn(`ReaceAce: editor option ${option} was activated but not found. Did you need to import a related tool or did you possibly mispell the option?`) } } this.handleOptions(this.props, editor); diff --git a/tests/src/ace.spec.js b/tests/src/ace.spec.js index e6f60daa..29263bcb 100644 --- a/tests/src/ace.spec.js +++ b/tests/src/ace.spec.js @@ -5,7 +5,6 @@ import ace from 'brace'; import { mount } from 'enzyme'; import AceEditor from '../../src/ace.jsx'; import brace from 'brace'; // eslint-disable-line no-unused-vars - describe('Ace Component', () => { // Required for the document.getElementById used by Ace can work in the test environment @@ -21,6 +20,14 @@ describe('Ace Component', () => { expect(wrapper).to.exist; }); + it('should trigger console warn if editorOption is called', () => { + const stub = sinon.stub(console, 'warn'); + const wrapper = mount(, mountOptions); + expect(wrapper).to.exist; + expect(console.warn.calledWith('ReaceAce: editor option enableBasicAutocompletion was activated but not found. Did you need to import a related tool or did you possibly mispell the option?') ).to.be.true; + stub.restore(); + }); + it('should render without problems with defaults properties, defaultValue and keyboardHandler', () => { const wrapper = mount( { expect(onFocusCallback.callCount).to.equal(1); }); + }); }); diff --git a/tests/src/split.spec.js b/tests/src/split.spec.js index 4a47e5fc..19203f06 100644 --- a/tests/src/split.spec.js +++ b/tests/src/split.spec.js @@ -15,7 +15,7 @@ describe('Split Component', () => { }; describe('General', () => { - + sinon.restore() it('should render without problems with defaults properties', () => { const wrapper = mount(, mountOptions); expect(wrapper).to.exist; @@ -28,6 +28,14 @@ describe('Split Component', () => { expect(beforeLoadCallback.getCall(0).args[0]).to.deep.equal(ace); }); + it('should trigger console warn if editorOption is called', () => { + const stub = sinon.stub(console, 'warn'); + const wrapper = mount(, mountOptions); + expect(wrapper).to.exist; + expect(console.warn.calledWith('ReaceAce: editor option enableBasicAutocompletion was activated but not found. Did you need to import a related tool or did you possibly mispell the option?') ).to.be.true; + stub.restore(); + }); + it('should set the editor props to the Ace element', () => { const editorProperties = { react: 'setFromReact', From caab1260ba031e96daa04c38850be7364dc35018 Mon Sep 17 00:00:00 2001 From: James Hrisho Date: Sat, 27 May 2017 21:17:13 -0400 Subject: [PATCH 05/10] onLoad should return the split editor --- src/split.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/split.jsx b/src/split.jsx index 4da872cb..48690fca 100644 --- a/src/split.jsx +++ b/src/split.jsx @@ -119,7 +119,7 @@ export default class SplitComponent extends Component { sp.setOrientation( this.props.orientation === 'below' ? sp.BELOW : sp.BESIDE); sp.resize(true) if (onLoad) { - onLoad(this.editor); + onLoad(sp); } } From 56fa7c040504e0e02e68795a7e0a9fb252813178 Mon Sep 17 00:00:00 2001 From: James Hrisho Date: Sun, 28 May 2017 07:39:22 -0400 Subject: [PATCH 06/10] Fix test and a ton of documentation --- README.md | 101 ++++------------------------------- docs/Ace.md | 45 ++++++++++++++++ docs/FAQ.md | 113 ++++++++++++++++++++++++++++++++++++++++ docs/Modes.md | 39 ++++++++++++++ docs/Split.md | 45 ++++++++++++++++ example/index.js | 1 + example/split.js | 2 +- tests/src/split.spec.js | 2 +- 8 files changed, 256 insertions(+), 92 deletions(-) create mode 100644 docs/Ace.md create mode 100644 docs/FAQ.md create mode 100644 docs/Modes.md create mode 100644 docs/Split.md diff --git a/README.md b/README.md index 21fd4496..b781ea3a 100644 --- a/README.md +++ b/README.md @@ -7,15 +7,16 @@ [![CDNJS](https://img.shields.io/cdnjs/v/react-ace.svg)](https://cdnjs.com/libraries/react-ace) [![Coverage Status](https://coveralls.io/repos/github/securingsincity/react-ace/badge.svg?branch=master)](https://coveralls.io/github/securingsincity/react-ace?branch=master) -A react component for Ace / Brace +A set of react components for Ace / Brace -[DEMO](http://securingsincity.github.io/react-ace/) +[DEMO of React Ace](http://securingsincity.github.io/react-ace/) +[DEMO of React Ace Split Editor](http://securingsincity.github.io/react-ace/split.html) ## Install `npm install react-ace` -## Usage +## Basic Usage ```javascript import React from 'react'; @@ -45,94 +46,14 @@ render( ## Examples -* Checkout `example` directory for a working example using webpack. -* [create-react-app](https://github.com/securingsincity/react-ace-create-react-app-example) -* [preact](https://github.com/securingsincity/react-ace-preact-example) -* [webpack](https://github.com/securingsincity/react-ace-webpack-example) - - -## Available Props - -|Prop|Default|Description| -|-----|------|----------| -|name| 'brace-editor'| Unique Id to be used for the editor| -|mode| ''| Language for parsing and code highlighting| -|theme| ''| theme to use| -|height| '500px'| CSS value for height| -|width| '500px'| CSS value for width| -|className| | custom className| -|fontSize| 12| pixel value for font-size| -|showGutter| true| boolean| -|showPrintMargin| true| boolean| -|highlightActiveLine| true| boolean| -|focus| false| boolean| -|cursorStart| 1| number| -|wrapEnabled| false| Wrapping lines| -|readOnly| false| boolean| -|minLines| | Minimum number of lines to be displayed| -|maxLines| | Maximum number of lines to be displayed| -|enableBasicAutocompletion| false| Enable basic autocompletion| -|enableLiveAutocompletion| false| Enable live autocompletion| -|tabSize| 4| tabSize number| -|value | ''| String value you want to populate in the code highlighter| -|defaultValue | ''| Default value of the editor| -|onLoad| | Function onLoad| -|onBeforeLoad| | function that trigger before editor setup| -|onChange| | function that occurs on document change it has 2 arguments the value and the event. see the example above| -|onCopy| | function that trigger by editor `copy` event, and pass text as argument| -|onPaste| | function that trigger by editor `paste` event, and pass text as argument| -|onSelectionChange| | function that trigger by editor `selectionChange` event, and passes a [Selection](https://ace.c9.io/#nav=api&api=selection) as it's first argument and the event as the second| -|onFocus| | function that trigger by editor `focus` event| -|onBlur| | function that trigger by editor `blur` event| -|onScroll| | function that trigger by editor `scroll` event| -|editorProps| | Object of properties to apply directly to the Ace editor instance| -|setOptions| | Object of [options](https://github.com/ajaxorg/ace/wiki/Configuring-Ace) to apply directly to the Ace editor instance| -|keyboardHandler| | String corresponding to the keybinding mode to set (such as vim)| -|commands| | Array of new commands to add to the editor -|annotations| | Array of annotations to show in the editor i.e. `[{ row: 0, column: 2, type: 'error', text: 'Some error.'}]`, displayed in the gutter| -|markers| | Array of [markers](https://ace.c9.io/api/edit_session.html#EditSession.addMarker) to show in the editor, i.e. `[{ startRow: 0, startCol: 2, endRow: 1, endCol: 20, className: 'error-marker', type: 'background' }]`| -|style| | Object with camelCased properties | - -## Modes, Themes, and Keyboard Handlers - -All modes, themes, and keyboard handlers should be required through ```brace``` directly. Browserify will grab these modes / themes / keyboard handlers through ```brace``` and will be available at run time. See the example above. This prevents bloating the compiled javascript with extra modes and themes for your application. - -### Example Modes - -* javascript -* java -* python -* xml -* ruby -* sass -* markdown -* mysql -* json -* html -* handlebars -* golang -* csharp -* coffee -* css - -### Example Themes - -* monokai -* github -* tomorrow -* kuroir -* twilight -* xcode -* textmate -* solarized dark -* solarized light -* terminal - -### Example Keyboard Handlers - -* vim -* emacs +Checkout the `example` directory for a working example using webpack. +## Documentation + +[Ace Editor](https://github.com/securingsincity/react-ace/blob/master/docs/Ace.md) +[Split View Editor](https://github.com/securingsincity/react-ace/blob/master/docs/Split.md) +[How to add modes, themes and keyboard handlers](https://github.com/securingsincity/react-ace/blob/master/docs/Modes.md) +[Frequently Asked Questions](https://github.com/securingsincity/react-ace/blob/master/docs/FAQ.md) ## Backers diff --git a/docs/Ace.md b/docs/Ace.md new file mode 100644 index 00000000..4de735fa --- /dev/null +++ b/docs/Ace.md @@ -0,0 +1,45 @@ +# Ace Editor + +This is the main component of React-Ace. It creates an instance of the Ace Editor. + +## Available Props + +|Prop|Default|Type|Description| +|-----|------|-----|-----| +|name| 'brace-editor'| String |Unique Id to be used for the editor| +|mode| ''| String |Language for parsing and code highlighting| +|theme| ''| String |theme to use| +|value | ''| String | value you want to populate in the code highlighter| +|defaultValue | ''| String |Default value of the editor| +|height| '500px'| String |CSS value for height| +|width| '500px'| String |CSS value for width| +|className| | String |custom className| +|fontSize| 12| Number |pixel value for font-size| +|showGutter| true| Boolean | show gutter | +|showPrintMargin| true| Boolean| show print margin | +|highlightActiveLine| true| Boolean| highlight active line| +|focus| false| Boolean| whether to focus +|cursorStart| 1| Number| the location of the cursor +|wrapEnabled| false| Boolean | Wrapping lines| +|readOnly| false| Boolean| make the editor read only | +|minLines| | Number |Minimum number of lines to be displayed| +|maxLines| | Number |Maximum number of lines to be displayed| +|enableBasicAutocompletion| false| Boolean | Enable basic autocompletion| +|enableLiveAutocompletion| false| Boolean | Enable live autocompletion| +|tabSize| 4| Number| tabSize| +|onLoad| | Function | called on editor load. The first argument is the instance of the editor | +|onBeforeLoad| | Function | called before editor load. the first argument is an instance of `ace`| +|onChange| | Function | occurs on document change it has 2 arguments the value and the event.| +|onCopy| | Function | triggered by editor `copy` event, and passes text as argument| +|onPaste| | Function | Triggered by editor `paste` event, and passes text as argument| +|onSelectionChange| | Function | triggered by editor `selectionChange` event, and passes a [Selection](https://ace.c9.io/#nav=api&api=selection) as it's first argument and the event as the second| +|onFocus| | Function | triggered by editor `focus` event| +|onBlur| | Function | triggered by editor `blur` event| +|onScroll| | Function | triggered by editor `scroll` event| +|editorProps| | Object | properties to apply directly to the Ace editor instance| +|setOptions| | Object | [options](https://github.com/ajaxorg/ace/wiki/Configuring-Ace) to apply directly to the Ace editor instance| +|keyboardHandler| | String | corresponding to the keybinding mode to set (such as vim or emacs)| +|commands| | Array | new commands to add to the editor +|annotations| | Array | annotations to show in the editor i.e. `[{ row: 0, column: 2, type: 'error', text: 'Some error.'}]`, displayed in the gutter| +|markers| | Array | [markers](https://ace.c9.io/api/edit_session.html#EditSession.addMarker) to show in the editor, i.e. `[{ startRow: 0, startCol: 2, endRow: 1, endCol: 20, className: 'error-marker', type: 'background' }]`| +|style| | Object | camelCased properties | diff --git a/docs/FAQ.md b/docs/FAQ.md new file mode 100644 index 00000000..364fffdb --- /dev/null +++ b/docs/FAQ.md @@ -0,0 +1,113 @@ +# Frequently Asked Questions + +## How do I use it with `preact`? `webpack`? `create-react-app`? + +Check out the example applications + +* [create-react-app](https://github.com/securingsincity/react-ace-create-react-app-example) +* [preact](https://github.com/securingsincity/react-ace-preact-example) +* [webpack](https://github.com/securingsincity/react-ace-webpack-example) + + +## How do call methods on the editor? How do I call Undo or Redo? + +`ReactAce` has an editor property, which is the wrapped editor. You can use refs to get to the component, and then you should be able to use the editor on the component to run the function you need: + +```javascript +const reactAceComponent = parent.refs.reactAceComponent; +const editor = reactAceComponent.editor +editor.find(searchRegex, { + backwards: false, + wrap: true, + caseSensitive: false, + wholeWord: false, + regExp: true, +}); +``` + +Similarly, if you want to redo or undo, you can reference the editor from the refs + +```jsx + + +``` + +## How do I set editor options like setting block scrolling to infinity? + +```javascript + +``` + +## How do I add language snippets? + +You can import the snippets and mode directly through `brace` along with the language_tools. Here is an example below + + +```javascript +import React from 'react'; +import { render } from 'react-dom'; +import brace from 'brace'; +import AceEditor from 'react-ace'; + +import 'brace/mode/python'; +import 'brace/snippets/python'; +import 'brace/ext/language_tools'; +import 'brace/theme/github'; + +function onChange(newValue) { + console.log('change',newValue); +} + +// Render editor +render( + , + document.getElementById('example') +); +``` + +## How do I add custom completers? + +## How do I add markers? +```javascript + const markers = [{ + startRow: 3, + type: 'text', + className: 'test-marker' + }]; + const wrapper = (); +``` + +## How do I add annotations? +```javascript + const annotations = [{ + row: 3, // must be 0 based + column: 4, // must be 0 based + text: 'error.message', // text to show in tooltip + type: 'error' + }] + const editor = ( + + ) +``` + +## How do I add the search box? +Add the following line + +`import 'brace/ext/searchbox';` + +before introducing the component and it will add the search box. \ No newline at end of file diff --git a/docs/Modes.md b/docs/Modes.md new file mode 100644 index 00000000..9c2c3950 --- /dev/null +++ b/docs/Modes.md @@ -0,0 +1,39 @@ +# Modes, Themes, and Keyboard Handlers + +All modes, themes, and keyboard handlers should be required through ```brace``` directly. Browserify will grab these modes / themes / keyboard handlers through ```brace``` and will be available at run time. See the example above. This prevents bloating the compiled javascript with extra modes and themes for your application. + +### Example Modes + +* javascript +* java +* python +* xml +* ruby +* sass +* markdown +* mysql +* json +* html +* handlebars +* golang +* csharp +* coffee +* css + +### Example Themes + +* monokai +* github +* tomorrow +* kuroir +* twilight +* xcode +* textmate +* solarized dark +* solarized light +* terminal + +### Example Keyboard Handlers + +* vim +* emacs \ No newline at end of file diff --git a/docs/Split.md b/docs/Split.md new file mode 100644 index 00000000..061a83cf --- /dev/null +++ b/docs/Split.md @@ -0,0 +1,45 @@ +# Split Editor + +This allows for a split editor which can create multiple linked instances of the Ace editor. Each instance shares a theme and other properties while having their own value. + +## Available Props + +|Prop|Default|Type|Description| +|-----|------|-----|-----| +|name| 'brace-editor'| String |Unique Id to be used for the editor| +|mode| ''| String |Language for parsing and code highlighting| +|theme| ''| String |theme to use| +|value | ''| Array of Strings | value you want to populate in each code editor| +|defaultValue | ''| Array of Strings |Default value for each editor| +|height| '500px'| String |CSS value for height| +|width| '500px'| String |CSS value for width| +|className| | String |custom className| +|fontSize| 12| Number |pixel value for font-size| +|showGutter| true| Boolean | show gutter | +|showPrintMargin| true| Boolean| show print margin | +|highlightActiveLine| true| Boolean| highlight active line| +|focus| false| Boolean| whether to focus +|cursorStart| 1| Number| the location of the cursor +|wrapEnabled| false| Boolean | Wrapping lines| +|readOnly| false| Boolean| make the editor read only | +|minLines| | Number |Minimum number of lines to be displayed| +|maxLines| | Number |Maximum number of lines to be displayed| +|enableBasicAutocompletion| false| Boolean | Enable basic autocompletion| +|enableLiveAutocompletion| false| Boolean | Enable live autocompletion| +|tabSize| 4| Number| tabSize| +|onLoad| | Function | called on editor load. The first argument is the instance of the editor | +|onBeforeLoad| | Function | called before editor load. the first argument is an instance of `ace`| +|onChange| | Function | occurs on document change it has 2 arguments the value of each editor and the event.| +|onCopy| | Function | triggered by editor `copy` event, and passes text as argument| +|onPaste| | Function | Triggered by editor `paste` event, and passes text as argument| +|onSelectionChange| | Function | triggered by editor `selectionChange` event, and passes a [Selection](https://ace.c9.io/#nav=api&api=selection) as it's first argument and the event as the second| +|onFocus| | Function | triggered by editor `focus` event| +|onBlur| | Function | triggered by editor `blur` event| +|onScroll| | Function | triggered by editor `scroll` event| +|editorProps| | Object | properties to apply directly to the Ace editor instance| +|setOptions| | Object | [options](https://github.com/ajaxorg/ace/wiki/Configuring-Ace) to apply directly to the Ace editor instance| +|keyboardHandler| | String | corresponding to the keybinding mode to set (such as vim or emacs)| +|commands| | Array | new commands to add to the editor +|annotations| | Array of Arrays | annotations to show in the editor i.e. `[{ row: 0, column: 2, type: 'error', text: 'Some error.'}]`, displayed in the gutter| +|markers| | Array of Arrays | [markers](https://ace.c9.io/api/edit_session.html#EditSession.addMarker) to show in the editor, i.e. `[{ startRow: 0, startCol: 2, endRow: 1, endCol: 20, className: 'error-marker', type: 'background' }]`| +|style| | Object | camelCased properties | diff --git a/example/index.js b/example/index.js index fe6c9872..dfa0aa53 100644 --- a/example/index.js +++ b/example/index.js @@ -45,6 +45,7 @@ themes.forEach((theme) => { }) /*eslint-disable no-alert, no-console */ import 'brace/ext/language_tools'; +import 'brace/ext/searchbox'; const defaultValue = diff --git a/example/split.js b/example/split.js index a71b80ce..6465da39 100644 --- a/example/split.js +++ b/example/split.js @@ -3,7 +3,7 @@ import { render } from 'react-dom'; import SplitAceEditor from '../src/split.jsx'; import 'brace/mode/jsx'; - +import 'brace/ext/searchbox'; const languages = [ 'javascript', diff --git a/tests/src/split.spec.js b/tests/src/split.spec.js index 19203f06..44b6b053 100644 --- a/tests/src/split.spec.js +++ b/tests/src/split.spec.js @@ -106,7 +106,7 @@ describe('Split Component', () => { const wrapper = mount(, mountOptions); // Get the editor - const editor = wrapper.instance().editor; + const editor = wrapper.instance().split; expect(loadCallback.callCount).to.equal(1); expect(loadCallback.getCall(0).args[0]).to.deep.equal(editor); From 8cecf02289cec68617c448b7e9c0d3b164100148 Mon Sep 17 00:00:00 2001 From: James Hrisho Date: Sun, 28 May 2017 08:16:20 -0400 Subject: [PATCH 07/10] Add test for getting selected text --- tests/src/ace.spec.js | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/tests/src/ace.spec.js b/tests/src/ace.spec.js index 29263bcb..81a55aca 100644 --- a/tests/src/ace.spec.js +++ b/tests/src/ace.spec.js @@ -240,17 +240,23 @@ describe('Ace Component', () => { expect(onFocusCallback.callCount).to.equal(1); }); - it('should call the onSelectionChange method callback', () => { - const onSelectionChangeCallback = sinon.spy(); - const wrapper = mount(, mountOptions); - - // Check is not previously called - expect(onSelectionChangeCallback.callCount).to.equal(0); - - // Trigger the focus event + it('should call the onSelectionChange method callback', (done) => { + let onSelectionChange = function(){} + const value = ` + function main(value) { + console.log('hi james') + return value; + } + `; + const wrapper = mount(, mountOptions); + + onSelectionChange = function(selection) { + const content = wrapper.instance().editor.session.getTextRange(selection.getRange()); + expect(content).to.equal(value) + done() + } + wrapper.setProps({onSelectionChange}) wrapper.instance().editor.getSession().selection.selectAll() - - expect(onSelectionChangeCallback.callCount).to.equal(1); }); it('should call the onBlur method callback', () => { From f0413420ab377dc5ab0710099ae053a141d406ec Mon Sep 17 00:00:00 2001 From: James Hrisho Date: Sun, 28 May 2017 08:21:22 -0400 Subject: [PATCH 08/10] Example on how to get selected text --- docs/FAQ.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/FAQ.md b/docs/FAQ.md index 364fffdb..d8dd2964 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -78,7 +78,18 @@ render( ); ``` -## How do I add custom completers? +## How do I get selected text `onSelectionChange`? + +How you extract the text from the editor is based on how to call methods on the editor. + +Your `onSelectionChange` should look like this: + +```javascript +onSelectionChange(selection) { + const content = this.refs.aceEditor.editor.session.getTextRange(selection.getRange()); + // use content +} +``` ## How do I add markers? ```javascript From c5fa55ed018c4af1049856a3b76a63a7a355eb38 Mon Sep 17 00:00:00 2001 From: James Hrisho Date: Sun, 28 May 2017 08:26:09 -0400 Subject: [PATCH 09/10] Example of how to add a custom mode --- docs/FAQ.md | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/docs/FAQ.md b/docs/FAQ.md index d8dd2964..d14c1526 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -121,4 +121,39 @@ Add the following line `import 'brace/ext/searchbox';` -before introducing the component and it will add the search box. \ No newline at end of file +before introducing the component and it will add the search box. + +## How do I add a custom mode? + +1. Create my custom mode class (pure ES6 code) +2. Initialize the component with an existing mode name (such as "sql") +3. Use the `componentDidMount` function and call `session.setMode` with an instance of my custom mode. + +My custom mode is: +```javascript +export default class CustomSqlMode extends ace.acequire('ace/mode/text').Mode { + constructor(){ + super(); + // Your code goes here + } +} +``` + +And my react-ace code looks like: +```javascript +render() { + return
+ +
; +} + +componentDidMount() { + const customMode = new CustomSqlMode(); + this.refs.aceEditor.editor.getSession().setMode(customMode); +} +``` + From efac45d0f294236553cd6469a8fceeb6f55193c9 Mon Sep 17 00:00:00 2001 From: James Hrisho Date: Sun, 28 May 2017 08:30:24 -0400 Subject: [PATCH 10/10] Ups package version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ea1eb6b7..a33944ef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-ace", - "version": "4.4.0", + "version": "5.0.0", "description": "A react component for Ace Editor", "main": "lib/index.js", "types": "types.d.ts",