├── .babelrc ├── .eslintrc ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .husky └── pre-commit ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── docs ├── Ace.md ├── Diff.md ├── FAQ.md ├── Modes.md └── Split.md ├── example ├── diff.css ├── diff.html ├── diff.js ├── index.html ├── index.js ├── split.html └── split.js ├── logo.png ├── package-lock.json ├── package.json ├── src ├── ace.js ├── diff.js ├── editorOptions.js ├── index.js └── split.js ├── tests ├── setup.js └── src │ ├── ace.spec.js │ └── split.spec.js ├── types.d.ts ├── webpack-resolver-min.js └── webpack.config.example.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@babel/eslint-parser", 3 | "extends": ["eslint:recommended", "plugin:react/recommended"], 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "es6": true 8 | }, 9 | "parserOptions": { 10 | "sourceType": "module" 11 | }, 12 | "plugins": [ 13 | "react" 14 | ], 15 | "settings": { 16 | "react": { 17 | "version": "detect" 18 | } 19 | }, 20 | "globals": { 21 | "ENVIRONMENT": true, 22 | "STANDALONE": true, 23 | "it": true, 24 | "describe": true, 25 | "xdescribe": true, 26 | "xit": true, 27 | "before": true, 28 | "beforeEach": true, 29 | "after": true, 30 | "afterEach": true 31 | }, 32 | "rules": { 33 | "linebreak-style": [ 34 | "error", 35 | "unix" 36 | ], 37 | "no-console": [ 38 | "error", 39 | { 40 | "allow": [ 41 | "warn", 42 | "error" 43 | ] 44 | } 45 | ], 46 | "react/no-deprecated": "warn" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | # Problem 3 | 4 | Detail the problem here, including any possible solutions. 5 | 6 | ## Sample code to reproduce your issue 7 | 8 | 9 | ## References 10 | 11 | Progress on: # 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build 25 | 26 | # Dependency directory 27 | # Commenting this out is preferred by some people, see 28 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 29 | node_modules 30 | 31 | # Users Environment Variables 32 | .lock-wscript 33 | 34 | # JetBrains IDE 35 | .idea 36 | 37 | # Visual Studios Code 38 | .vscode 39 | 40 | # Babel Build 41 | lib 42 | 43 | # UMD Build 44 | example/static 45 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pretty-quick --staged 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | example 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "16" 4 | install: 5 | - npm install 6 | script: 7 | - npm run build 8 | - npm run lint 9 | - npm run coverage 10 | - npm run build:example 11 | after_success: 12 | - cat ./coverage/*.info | ./node_modules/coveralls/bin/coveralls.js 13 | cache: 14 | directories: 15 | - node_modules 16 | deploy: 17 | - provider: releases 18 | api_key: "$GITHUB_TOKEN" 19 | skip_cleanup: true 20 | on: 21 | tags: true 22 | repo: manubb/react-ace-builds 23 | branch: local 24 | - provider: npm 25 | edge: true 26 | email: mbaclet@gmail.com 27 | api_key: 28 | secure: "RSFG8Xxgsuj8SKVlzV9gyW5Td6wX3Lbvq3n7LP9XSLw9PsEGeO57EoXhRkOfs6IEkDZww3wO4KxqQIYEXbm03C5W7g1ou9zE+utkk5niWmI4pnTTizz8jOnsUsdvLLq51ZeE6j1pUhdQkBw1m17xOIf+u2kmWk6EoavBB7Io7vXwg+Rzq//jTSLrISX0xH9njPEvqe+vkY3AUdXURQRqALAyLSgDghaCyJcIskcgUhYfLAcLRJhq868/VqsD07ze7A6bc+2H+0uQX2O4BXeAE/lyOOCZJWM7h6itSa7mVSk6EkpuemFHmga2T9OL5FEoxEpexA4O9A/dFBh4wflK8HcIAuT+eOTY2asEx6JtUd9pZn+IShpjw//gIBSExJ5p3TEgyRBViMFfi5q/jMXE/VL49NsHKVfeTRglyrLNf/Nppz/OGgivyUuZYr26qhW3Ie7NyHsiQ7gbF8YnqwbLcUhPEsOhFeEBtXs+oohengWveJ3M2pwCN/HhfTB3fFYP7NZng/5NaoZfwYiaCa7REProOWRWpcFSx+ZB3+ihYzdjcNQv/hfx9XqfwmiZwQgWUpwux3LkG42hckvzWW4xiED99i1LzX6/HdYMt7pPEwLGbsnYh02bzFkpGTF4Sr2NwCoeYeS387kAl7jTXZfZrPx2FVZZ3jJMzdiKws0w8R8=" 29 | skip_cleanup: true 30 | on: 31 | tags: true 32 | repo: manubb/react-ace-builds 33 | branch: local 34 | - provider: pages 35 | skip_cleanup: true 36 | github_token: "$GITHUB_TOKEN" 37 | on: 38 | branch: local 39 | repo: manubb/react-ace-builds 40 | local_dir: example 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 7.4.1 4 | 5 | - Use ace-builds@1.31.1 6 | 7 | ## 7.3.6 8 | 9 | - Use ace-builds@1.4.12 10 | 11 | ## 7.3.0 12 | 13 | - Use ace-builds@1.4.8 14 | 15 | ## 7.2.6 16 | 17 | - Use ace-builds@1.4.7 18 | - Bump dependencies 19 | 20 | ## 7.2.0 21 | 22 | - Remove umd build 23 | - Use babel@7 24 | - Add webpack-resolver-min.js to use ace minified modules 25 | 26 | ## 7.0.0 27 | 28 | - Replace brace with ace-builds 29 | 30 | ## 6.2.1 31 | 32 | - Add editor to onFocus event as per issue #389 33 | - Upgraded webpack 34 | - Add exec argument in ts #535 35 | - Prettier as part of the build 36 | 37 | ## 6.2.0 38 | 39 | - Support for React 17 40 | - Upgraded dependencies 41 | - AceOptions interface adds debounceChangePeriod 42 | - update types 43 | 44 | ## 6.1.4 45 | 46 | - Fixes #479 Diff component does not refresh when value prop changes 47 | 48 | ## 6.1.3 49 | 50 | - Fixes #300 where users were not able to set annotations for multiline text that is changed 51 | 52 | ## 6.1.2 53 | 54 | - Additional Diff documentation 55 | - Add className to diff 56 | - Add Logo to docs 57 | - upgrade dev dependencies 58 | 59 | ## 6.1.1 60 | 61 | - Fixes typo in `console.warn` 62 | - Adds style property to typings 63 | 64 | ## 6.1.0 65 | 66 | - Onchange support in diff editor 67 | - Debounce Prop support in split editor 68 | 69 | ## 6.0.0 70 | 71 | - Adds Diff editor 72 | 73 | ## 5.10.0 74 | 75 | - Upgraded many build dependencies 76 | - Split editor adds UndoManager 77 | 78 | ## 5.9.0 79 | 80 | - First value resets undo manager. Closes #339 and #223 81 | - Updated split editor documentation 82 | 83 | ## 5.8.0 84 | 85 | - Upgrade brace to 0.11 86 | - More loose comparison for componentDidMount for default value. Closes #317. Thanks @VijayKrish93 87 | 88 | ## 5.7.0 89 | 90 | - Adds debounce option for onChange event 91 | - Add support onCursorChange event 92 | - Adds editor as second argument to the onBlur 93 | 94 | ## 5.5.0 95 | 96 | - Adds the onInput event 97 | 98 | ## 5.4.0 99 | 100 | - #285: Added the possibility to change key bindings of existing commands. thanks to @FurcyPin 101 | 102 | ## 5.3.0 103 | 104 | - Adds support for React 16 thanks to @layershifter 105 | - Removes react and react-dom from build. thanks to @M-ZubairAhmed 106 | 107 | ## 5.2.1 and 5.2.2 108 | 109 | - Remove Open Collective from build 110 | 111 | ## 5.2.0 112 | 113 | - Add support for events in onBlur and onFocus callbacks 114 | - Adds onValidate callback 115 | 116 | ## 5.1.2 117 | 118 | - Resize on component did mount and component did update. Fixes #207 and #212. 119 | 120 | ## 5.1.1 121 | 122 | - Fix TypeScript definitions for EditorProps 123 | 124 | ## 5.1.0 125 | 126 | - Editor options do not get reverted due to default props #226 127 | - Markers can be unset to an empty value #229 128 | - Typescript update to set state to empty object instead of undefined 129 | 130 | ## 5.0.1 131 | 132 | - Fixes file extension issue related to `5.0.0`. 133 | 134 | ## 5.0.0 135 | 136 | - Support for a Split View Editor - see more about the Split View editor [here](https://github.com/securingsincity/react-ace/blob/master/docs/Split.md) 137 | - Ace Editor will now warn on mispelled editor options 138 | - All new documentation 139 | 140 | ## 4.4.0 141 | 142 | - Ace's resize method will be called when the prop `width` changes 143 | 144 | ## 4.3.0 145 | 146 | - Adds support for `onSelectionChange` event 147 | - Add the `Event` as an optional argument to the `onChange` prop 148 | - All new examples 149 | 150 | ## 4.2.2 151 | 152 | - [bugfix] should not handle markers without any markers 153 | 154 | ## 4.2.1 155 | 156 | - Use `prop-type` package instead of React.PropType 157 | 158 | ## 4.2.0 159 | 160 | - Fix `ref` related error 161 | 162 | ## 4.1.6 163 | 164 | - Reverse `PureComponent` use in AceEditor back to `Component` 165 | 166 | ## 4.1.5 167 | 168 | - Add ability to set `scrollMargins` 169 | 170 | ## 4.1.4 171 | 172 | - TypeScript Definitions 173 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at james.hrisho@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Installing 4 | 5 | 1. Fork the repo 6 | 1. clone the repo locally 7 | 1. install the dependencies 8 | ``` 9 | npm install react 10 | npm install react-dom 11 | npm install 12 | ``` 13 | 1. build the application `npm run build` 14 | 1. run the example `npm run example` 15 | 16 | 17 | ## How to add a new feature or fix a bug 18 | 19 | 1. check out a new branch `git checkout -b ` 20 | 1. Write your code and tests 21 | 1. Open a pull request following our pull request template 22 | 1. The pull request should meet these standards 23 | - Code coverage remains at least as high as it was when you started. 24 | - Add necessary documentation. 25 | - Tests all pass. 26 | - The dependencies remain up to date. 27 | 1. Code will be reviewed before being merged. Code will not be reviewed until all checks pass 28 | 29 | ## How to open a new issue 30 | 31 | 1. Ensure the bug was not already reported by searching on GitHub under Issues. 32 | 1. Follow the issue template 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 James Hrisho 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # What's in this PR? 2 | 3 | ## List the changes you made and your reasons for them. 4 | 5 | Make sure any changes to code include changes to documentation. 6 | 7 | ## References 8 | 9 | ### Fixes # 10 | 11 | ### Progress on: # -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React-Ace-Builds 2 | 3 | ![logo](https://github.com/manubb/react-ace-builds/raw/local/logo.png) 4 | 5 | [![npm version](https://badge.fury.io/js/react-ace-builds.svg)](http://badge.fury.io/js/react-ace-builds) 6 | [![Build Status](https://app.travis-ci.com/manubb/react-ace-builds.svg)](https://app.travis-ci.com/manubb/react-ace-builds) 7 | [![jsdeliver](https://data.jsdelivr.com/v1/package/npm/react-ace-builds/badge)](https://www.jsdelivr.com/package/npm/react-ace-builds) 8 | [![Coverage Status](https://coveralls.io/repos/github/manubb/react-ace-builds/badge.svg)](https://coveralls.io/github/manubb/react-ace-builds) 9 | 10 | A set of react components for Ace 11 | 12 | [DEMO of React Ace-Builds](http://manubb.github.io/react-ace-builds/) 13 | 14 | [DEMO of React Ace-Builds Split Editor](http://manubb.github.io/react-ace-builds/split.html) 15 | 16 | [DEMO of React Ace-Builds Diff Editor](http://manubb.github.io/react-ace-builds/diff.html) 17 | 18 | ## Notice 19 | 20 | This repository contains a fork of [securingsincity/react-ace](https://github.com/securingsincity/react-ace) where unmaintained [brace](https://github.com/thlorenz/brace) is replaced with [ace-builds](https://github.com/ajaxorg/ace-builds). This was motivated by a [pull request](https://github.com/securingsincity/react-ace/pull/540) created by [@dennisoelkers](https://github.com/dennisoelkers). 21 | 22 | ## Install 23 | 24 | `npm install react-ace-builds` 25 | 26 | ## Basic Usage 27 | 28 | ```javascript 29 | import React from "react"; 30 | import { render } from "react-dom"; 31 | import AceEditor from "react-ace-builds"; 32 | import "react-ace-builds/webpack-resolver-min"; 33 | 34 | function onChange(newValue) { 35 | console.log("change", newValue); 36 | } 37 | 38 | // Render editor 39 | render( 40 | , 46 | document.getElementById("example") 47 | ); 48 | ``` 49 | 50 | ## Examples 51 | 52 | Checkout the `example` directory for a working example using webpack. 53 | 54 | ## Documentation 55 | 56 | [Ace Editor](https://github.com/manubb/react-ace-builds/blob/local/docs/Ace.md) 57 | 58 | [Split View Editor](https://github.com/manubb/react-ace-builds/blob/local/docs/Split.md) 59 | 60 | [Diff Editor](https://github.com/manubb/react-ace-builds/blob/local/docs/Diff.md) 61 | 62 | [How to add modes, themes and keyboard handlers](https://github.com/manubb/react-ace-builds/blob/local/docs/Modes.md) 63 | 64 | [Frequently Asked Questions](https://github.com/manubb/react-ace-builds/blob/local/docs/FAQ.md) 65 | -------------------------------------------------------------------------------- /docs/Ace.md: -------------------------------------------------------------------------------- 1 | # Ace Editor 2 | 3 | This is the main component of React-Ace-Builds. It creates an instance of the Ace Editor. 4 | 5 | ## Demo 6 | 7 | https://manubb.github.io/react-ace-builds/ 8 | 9 | ## Example Code 10 | 11 | ```javascript 12 | import React from "react"; 13 | import { render } from "react-dom"; 14 | import AceEditor from "react-ace-builds"; 15 | import "react-ace-builds/webpack-resolver-min"; 16 | 17 | function onChange(newValue) { 18 | console.log("change", newValue); 19 | } 20 | 21 | // Render editor 22 | render( 23 | , 29 | document.getElementById("example") 30 | ); 31 | ``` 32 | 33 | ## Available Props 34 | 35 | | Prop | Default | Type | Description | 36 | | ------------------------- | ------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 37 | | name | 'ace-editor' | String | Unique Id to be used for the editor | 38 | | mode | '' | String | Language for parsing and code highlighting | 39 | | theme | '' | String | theme to use | 40 | | value | '' | String | value you want to populate in the code highlighter | 41 | | defaultValue | '' | String | Default value of the editor | 42 | | height | '500px' | String | CSS value for height | 43 | | width | '500px' | String | CSS value for width | 44 | | className | | String | custom className | 45 | | fontSize | 12 | Number | pixel value for font-size | 46 | | showGutter | true | Boolean | show gutter | 47 | | showPrintMargin | true | Boolean | show print margin | 48 | | highlightActiveLine | true | Boolean | highlight active line | 49 | | focus | false | Boolean | whether to focus | 50 | | cursorStart | 1 | Number | the location of the cursor | 51 | | wrapEnabled | false | Boolean | Wrapping lines | 52 | | readOnly | false | Boolean | make the editor read only | 53 | | minLines | | Number | Minimum number of lines to be displayed | 54 | | maxLines | | Number | Maximum number of lines to be displayed | 55 | | enableBasicAutocompletion | false | Boolean | Enable basic autocompletion | 56 | | enableLiveAutocompletion | false | Boolean | Enable live autocompletion | 57 | | tabSize | 4 | Number | tabSize | 58 | | debounceChangePeriod | null | Number | A debounce delay period for the onChange event | 59 | | onLoad | | Function | called on editor load. The first argument is the instance of the editor | 60 | | onBeforeLoad | | Function | called before editor load. the first argument is an instance of `ace` | 61 | | onChange | | Function | occurs on document change it has 2 arguments the value and the event. | 62 | | onCopy | | Function | triggered by editor `copy` event, and passes text as argument | 63 | | onPaste | | Function | Triggered by editor `paste` event, and passes text as argument | 64 | | 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 | 65 | | onCursorChange | | Function | triggered by editor `changeCursor` event, and passes a [Selection](https://ace.c9.io/#nav=api&api=selection) as it's first argument and the event as the second | 66 | | onFocus | | Function | triggered by editor `focus` event | 67 | | onBlur | | Function | triggered by editor `blur` event.It has two arguments event and editor | 68 | | onInput | | Function | triggered by editor `input` event | 69 | | onScroll | | Function | triggered by editor `scroll` event | 70 | | onValidate | | Function | triggered, when annotations are changed | 71 | | editorProps | | Object | properties to apply directly to the Ace editor instance | 72 | | setOptions | | Object | [options](https://github.com/ajaxorg/ace/wiki/Configuring-Ace) to apply directly to the Ace editor instance | 73 | | keyboardHandler | | String | corresponding to the keybinding mode to set (such as vim or emacs) | 74 | | commands | | Array | new commands to add to the editor | 75 | | annotations | | Array | annotations to show in the editor i.e. `[{ row: 0, column: 2, type: 'error', text: 'Some error.'}]`, displayed in the gutter | 76 | | markers | | Array | [markers](https://ace.c9.io/#nav=api&api=edit_session) to show in the editor, i.e. `[{ startRow: 0, startCol: 2, endRow: 1, endCol: 20, className: 'error-marker', type: 'background' }]`. Make sure to define the class (eg. ".error-marker") and set `position: absolute` for it. | 77 | | style | | Object | camelCased properties | 78 | -------------------------------------------------------------------------------- /docs/Diff.md: -------------------------------------------------------------------------------- 1 | # Diff Editor 2 | 3 | The diff editor is contained in a Split editor and will highlight differences between the two editor boxes. 4 | 5 | ## Demo 6 | 7 | https://manubb.github.io/react-ace-builds/diff.html 8 | 9 | ## Example Code 10 | 11 | ```javascript 12 | import React, { Component } from "react"; 13 | import { render } from "react-dom"; 14 | import { diff as DiffEditor } from "react-ace-builds"; 15 | import "react-ace-builds/webpack-resolver-min"; 16 | 17 | render( 18 | , 24 | document.getElementById("example") 25 | ); 26 | ``` 27 | 28 | Also see the [diff](../example/diff.js) [example](../example/diff.html) in the example folder for more robust sample code (seen in the [demo](https://manubb.github.io/react-ace-builds/diff.html)). 29 | 30 | ## Available Props 31 | 32 | | Prop | Default | Type | Description | 33 | | ------------------------- | ------------ | ---------------- | ----------------------------------------------------------------------------------------------------------- | 34 | | cursorStart | 1 | Number | the location of the cursor | 35 | | editorProps | | Object | properties to apply directly to the Ace editor instance | 36 | | enableBasicAutocompletion | false | Boolean | Enable basic autocompletion | 37 | | enableLiveAutocompletion | false | Boolean | Enable live autocompletion | 38 | | focus | false | Boolean | Whether to focus | 39 | | fontSize | 12 | Number | pixel value for font-size | 40 | | height | '500px' | String | CSS value for height | 41 | | highlightActiveLine | true | Boolean | highlight active line | 42 | | maxLines | | Number | Maximum number of lines to be displayed | 43 | | minLines | | Number | Minimum number of lines to be displayed | 44 | | mode | '' | String | The language to be used for the editor (Java, Javascript, Ruby, etc.) | 45 | | name | 'ace-editor' | string | Unique ID to be used for the split editor | 46 | | onLoad | | Function | called on editor load. The first argument is the instance of the editor | 47 | | onScroll | | Function | triggered by editor `scroll` event | 48 | | onChange | | Function | occurs on document change it has one argument the values array | 49 | | onPaste | | Function | Triggered by editor `paste` event, and passes text as argument | 50 | | orientation | 'beside' | String | The orientation of splits either 'beside' or 'below' | 51 | | readOnly | false | Boolean | make the editor read only | 52 | | scrollMargin | [0, 0, 0, 0] | Array of Numbers | Sets the scroll margins | 53 | | setOptions | | Object | [options](https://github.com/ajaxorg/ace/wiki/Configuring-Ace) to apply directly to the Ace editor instance | 54 | | showGutter | true | Boolean | show gutter | 55 | | showPrintMargin | true | Boolean | show print margin | 56 | | style | | Object | camelCased properties | 57 | | tabSize | 4 | Number | Number of spaces to include as tab | 58 | | theme | 'github' | String | Theme to use | 59 | | value | ['',''] | Array of Strings | Index 0: Value of first editor. Index 1: Value of second editor | 60 | | width | '500px' | String | CSS value for width | 61 | | wrapEnabled | true | Boolean | Whether lines wrap on the editor | 62 | -------------------------------------------------------------------------------- /docs/FAQ.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | ## How do call methods on the editor? How do I call Undo or Redo? 4 | 5 | `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: 6 | 7 | ```javascript 8 | const reactAceComponent = this.refs.reactAceComponent; 9 | const editor = reactAceComponent.editor; 10 | editor.find(searchRegex, { 11 | backwards: false, 12 | wrap: true, 13 | caseSensitive: false, 14 | wholeWord: false, 15 | regExp: true 16 | }); 17 | ``` 18 | 19 | Similarly, if you want to redo or undo, you can reference the editor from the refs 20 | 21 | ```jsx 22 | 23 | 24 | ``` 25 | 26 | ## How do I set editor options like setting block scrolling to infinity? 27 | 28 | ```javascript 29 | 34 | ``` 35 | 36 | ## How do I add language snippets? 37 | 38 | You can import the snippets through `ace-builds` along with the language_tools. Here is an example below 39 | 40 | ```javascript 41 | import React from "react"; 42 | import { render } from "react-dom"; 43 | import AceEditor from "react-ace-builds"; 44 | import "react-ace-builds/webpack-resolver-min"; 45 | import "ace-builds/src-noconflict/ext-language_tools.js"; 46 | 47 | ace.config.setModuleUrl( 48 | "ace/snippets/python", 49 | require("file-loader!ace-builds/src-noconflict/snippets/python.js") 50 | ); 51 | 52 | function onChange(newValue) { 53 | console.log("change", newValue); 54 | } 55 | 56 | // Render editor 57 | render( 58 | , 67 | document.getElementById("example") 68 | ); 69 | ``` 70 | 71 | ## How do I get selected text `onSelectionChange`? 72 | 73 | How you extract the text from the editor is based on how to call methods on the editor. 74 | 75 | Your `onSelectionChange` should look like this: 76 | 77 | ```javascript 78 | onSelectionChange(selection) { 79 | const content = this.refs.aceEditor.editor.session.getTextRange(selection.getRange()); 80 | // use content 81 | } 82 | ``` 83 | 84 | ## How do I get selected text ? 85 | 86 | ```javascript 87 | const selectedText = this.refs.aceEditor.editor.getSelectedText(); 88 | // selectedText contains the selected text. 89 | } 90 | ``` 91 | 92 | ## How do I add markers? 93 | 94 | ```javascript 95 | const markers = [ 96 | { 97 | startRow: 3, 98 | type: "text", 99 | className: "test-marker" 100 | } 101 | ]; 102 | const wrapper = ; 103 | ``` 104 | 105 | ## How do I add annotations? 106 | 107 | ```javascript 108 | const annotations = [ 109 | { 110 | row: 3, // must be 0 based 111 | column: 4, // must be 0 based 112 | text: "error.message", // text to show in tooltip 113 | type: "error" 114 | } 115 | ]; 116 | const editor = ; 117 | ``` 118 | 119 | ## How do I add key-bindings? 120 | 121 | ```javascript 122 | render() { 123 | return
124 | { console.log('key-binding used')} //function to execute when keys are pressed. 132 | }]} 133 | /> 134 |
; 135 | } 136 | ``` 137 | 138 | ## How do I change key-bindings for an existing command? 139 | 140 | Same syntax as above, where `exec` is given the name of the command to rebind. 141 | 142 | ```javascript 143 | render() { 144 | return
145 | 155 |
; 156 | } 157 | ``` 158 | 159 | ## How do I add the search box? 160 | 161 | The search box is lazy loaded when needed if you use: 162 | 163 | `import "react-ace-builds/webpack-resolver-min";` 164 | 165 | You can also import it directly: 166 | 167 | `import "ace-builds/src-min-noconflict/ext-searchbox"` 168 | 169 | ## How do I add a custom mode? 170 | 171 | 1. Create my custom mode class (pure ES6 code) 172 | 2. Initialize the component with an existing mode name (such as "sql") 173 | 3. Use the `componentDidMount` function and call `session.setMode` with an instance of my custom mode. 174 | 175 | My custom mode is: 176 | 177 | ```javascript 178 | export default class CustomSqlMode extends ace.require("ace/mode/text").Mode { 179 | constructor() { 180 | super(); 181 | // Your code goes here 182 | } 183 | } 184 | ``` 185 | 186 | And my react-ace code looks like: 187 | 188 | ```javascript 189 | render() { 190 | return
191 | 196 |
; 197 | } 198 | 199 | componentDidMount() { 200 | const customMode = new CustomSqlMode(); 201 | this.refs.aceEditor.editor.getSession().setMode(customMode); 202 | } 203 | ``` 204 | -------------------------------------------------------------------------------- /docs/Modes.md: -------------------------------------------------------------------------------- 1 | # Modes, Themes, and Keyboard Handlers 2 | 3 | All modes, themes and keyboard handlers will be lazy loaded when needed if you use: 4 | 5 | `import "react-ace-builds/webpack-resolver-min";` 6 | 7 | Snippets and language_tools should be manually imported to be used. See the main [example](../example/index.js) in the example folder for usage example (seen in the [demo](https://manubb.github.io/react-ace-builds)). 8 | 9 | ### Example Modes 10 | 11 | - javascript 12 | - java 13 | - python 14 | - xml 15 | - ruby 16 | - sass 17 | - markdown 18 | - mysql 19 | - json 20 | - html 21 | - handlebars 22 | - golang 23 | - csharp 24 | - coffee 25 | - css 26 | 27 | ### Example Themes 28 | 29 | - monokai 30 | - github 31 | - tomorrow 32 | - kuroir 33 | - twilight 34 | - xcode 35 | - textmate 36 | - solarized dark 37 | - solarized light 38 | - terminal 39 | 40 | ### Example Keyboard Handlers 41 | 42 | - vim 43 | - emacs 44 | -------------------------------------------------------------------------------- /docs/Split.md: -------------------------------------------------------------------------------- 1 | # Split Editor 2 | 3 | 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. 4 | 5 | ## Demo 6 | 7 | https://manubb.github.io/react-ace-builds/split.html 8 | 9 | ## Example Code 10 | 11 | ```javascript 12 | import React from "react"; 13 | import { render } from "react-dom"; 14 | import { split as SplitEditor } from "react-ace-builds"; 15 | import "react-ace-builds/webpack-resolver-min"; 16 | 17 | // Render editor 18 | render( 19 | , 27 | document.getElementById("example") 28 | ); 29 | ``` 30 | 31 | ## Available Props 32 | 33 | | Prop | Default | Type | Description | 34 | | ------------------------- | ------------ | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 35 | | name | 'ace-editor' | String | Unique Id to be used for the editor | 36 | | mode | '' | String | Language for parsing and code highlighting | 37 | | splits | 2 | Number | Number of views to have | 38 | | orientation | 'beside' | String | The orientation of the splits either `beside` or `below` | 39 | | theme | '' | String | theme to use | 40 | | value | '' | Array of Strings | value you want to populate in each code editor | 41 | | defaultValue | '' | Array of Strings | Default value for each editor | 42 | | height | '500px' | String | CSS value for height | 43 | | width | '500px' | String | CSS value for width | 44 | | className | | String | custom className | 45 | | fontSize | 12 | Number | pixel value for font-size | 46 | | showGutter | true | Boolean | show gutter | 47 | | showPrintMargin | true | Boolean | show print margin | 48 | | highlightActiveLine | true | Boolean | highlight active line | 49 | | focus | false | Boolean | whether to focus | 50 | | cursorStart | 1 | Number | the location of the cursor | 51 | | wrapEnabled | false | Boolean | Wrapping lines | 52 | | readOnly | false | Boolean | make the editor read only | 53 | | minLines | | Number | Minimum number of lines to be displayed | 54 | | maxLines | | Number | Maximum number of lines to be displayed | 55 | | enableBasicAutocompletion | false | Boolean | Enable basic autocompletion | 56 | | enableLiveAutocompletion | false | Boolean | Enable live autocompletion | 57 | | tabSize | 4 | Number | tabSize | 58 | | debounceChangePeriod | null | Number | A debounce delay period for the onChange event | 59 | | onLoad | | Function | called on editor load. The first argument is the instance of the editor | 60 | | onBeforeLoad | | Function | called before editor load. the first argument is an instance of `ace` | 61 | | onChange | | Function | occurs on document change it has 2 arguments the value of each editor and the event. | 62 | | onCopy | | Function | triggered by editor `copy` event, and passes text as argument | 63 | | onPaste | | Function | Triggered by editor `paste` event, and passes text as argument | 64 | | 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 | 65 | | onCursorChange | | Function | triggered by editor `changeCursor` event, and passes a [Selection](https://ace.c9.io/#nav=api&api=selection) as it's first argument and the event as the second | 66 | | onFocus | | Function | triggered by editor `focus` event | 67 | | onBlur | | Function | triggered by editor `blur` event | 68 | | onInput | | Function | triggered by editor `input` event | 69 | | onScroll | | Function | triggered by editor `scroll` event | 70 | | editorProps | | Object | properties to apply directly to the Ace editor instance | 71 | | setOptions | | Object | [options](https://github.com/ajaxorg/ace/wiki/Configuring-Ace) to apply directly to the Ace editor instance | 72 | | keyboardHandler | | String | corresponding to the keybinding mode to set (such as vim or emacs) | 73 | | commands | | Array | new commands to add to the editor | 74 | | 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 | 75 | | 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' }]` | 76 | | style | | Object | camelCased properties | 77 | -------------------------------------------------------------------------------- /example/diff.css: -------------------------------------------------------------------------------- 1 | .codeMarker { 2 | background: #fff677; 3 | position: absolute; 4 | z-index: 20; 5 | } 6 | -------------------------------------------------------------------------------- /example/diff.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Diff Editor 6 | 7 | 8 | 9 |
10 |
11 |

React-Ace: Diff Example

12 |
13 |
14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /example/diff.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { diff as DiffEditor } from '../src'; 4 | import '../webpack-resolver-min'; 5 | 6 | const defaultValue = [ 7 | `// Use this tool to display differences in code. 8 | // Deletions will be highlighted on the left, insertions highlighted on the right.`, 9 | `// Use this too to show difference in code. 10 | // Deletions will be highlighted on the left, insertions highlighted on the right. 11 | // The diff highlighting style can be altered in CSS. 12 | `, 13 | ]; 14 | 15 | const languages = [ 16 | 'javascript', 17 | 'java', 18 | 'python', 19 | 'xml', 20 | 'ruby', 21 | 'sass', 22 | 'markdown', 23 | 'mysql', 24 | 'json', 25 | 'html', 26 | 'handlebars', 27 | 'golang', 28 | 'csharp', 29 | 'elixir', 30 | 'typescript', 31 | 'css', 32 | ]; 33 | 34 | class App extends Component { 35 | constructor(props) { 36 | super(props); 37 | this.state = { 38 | value: defaultValue, 39 | fontSize: 14, 40 | mode: 'javascript', 41 | }; 42 | this.onChange = this.onChange.bind(this); 43 | this.setMode = this.setMode.bind(this); 44 | } 45 | 46 | onChange(newValue) { 47 | this.setState({ 48 | value: newValue, 49 | }); 50 | } 51 | 52 | setMode(e) { 53 | this.setState({ 54 | mode: e.target.value, 55 | }); 56 | } 57 | 58 | render() { 59 | return ( 60 |
61 |
62 |
63 | 64 |

65 | 66 | 73 | 74 |

75 |
76 |
77 |
78 |
79 |

Editor

80 | 87 |
88 |
89 | ); 90 | } 91 | } 92 | 93 | render(, document.getElementById('example')); 94 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Document 6 | 7 | 8 | 9 |
10 |
11 |

React-Ace-Builds

12 |
13 |
14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import AceEditor from '../src/ace'; 4 | import '../webpack-resolver-min'; 5 | import 'ace-builds/src-noconflict/ext-language_tools'; 6 | 7 | const languages = [ 8 | 'javascript', 9 | 'java', 10 | 'python', 11 | 'xml', 12 | 'ruby', 13 | 'sass', 14 | 'markdown', 15 | 'mysql', 16 | 'json', 17 | 'html', 18 | 'handlebars', 19 | 'golang', 20 | 'csharp', 21 | 'elixir', 22 | 'typescript', 23 | 'css', 24 | ]; 25 | 26 | languages.forEach((lang) => { 27 | ace.config.setModuleUrl( 28 | `ace/snippets/${lang}`, 29 | require(`file-loader!ace-builds/src-min-noconflict/snippets/${lang}`), 30 | ); 31 | }); 32 | 33 | const themes = [ 34 | 'monokai', 35 | 'github', 36 | 'tomorrow', 37 | 'kuroir', 38 | 'twilight', 39 | 'xcode', 40 | 'textmate', 41 | 'solarized_dark', 42 | 'solarized_light', 43 | 'terminal', 44 | ]; 45 | 46 | const defaultValue = `function onLoad(editor) { 47 | console.log("i've loaded"); 48 | }`; 49 | class App extends Component { 50 | onLoad() { 51 | console.log("i've loaded"); 52 | } 53 | onChange(newValue) { 54 | console.log('change', newValue); 55 | this.setState({ 56 | value: newValue, 57 | }); 58 | } 59 | 60 | onSelectionChange(newValue, event) { 61 | console.log('select-change', newValue); 62 | console.log('select-change-event', event); 63 | } 64 | 65 | onCursorChange(newValue, event) { 66 | console.log('cursor-change', newValue); 67 | console.log('cursor-change-event', event); 68 | } 69 | 70 | onValidate(annotations) { 71 | console.log('onValidate', annotations); 72 | } 73 | 74 | setTheme(e) { 75 | this.setState({ 76 | theme: e.target.value, 77 | }); 78 | } 79 | setMode(e) { 80 | this.setState({ 81 | mode: e.target.value, 82 | }); 83 | } 84 | setBoolean(name, value) { 85 | this.setState({ 86 | [name]: value, 87 | }); 88 | } 89 | setFontSize(e) { 90 | this.setState({ 91 | fontSize: parseInt(e.target.value, 10), 92 | }); 93 | } 94 | constructor(props) { 95 | super(props); 96 | this.state = { 97 | value: defaultValue, 98 | theme: 'monokai', 99 | mode: 'javascript', 100 | enableBasicAutocompletion: false, 101 | enableLiveAutocompletion: false, 102 | fontSize: 14, 103 | showGutter: true, 104 | showPrintMargin: true, 105 | highlightActiveLine: true, 106 | enableSnippets: false, 107 | showLineNumbers: true, 108 | }; 109 | this.setTheme = this.setTheme.bind(this); 110 | this.setMode = this.setMode.bind(this); 111 | this.onChange = this.onChange.bind(this); 112 | this.setFontSize = this.setFontSize.bind(this); 113 | this.setBoolean = this.setBoolean.bind(this); 114 | } 115 | render() { 116 | return ( 117 |
118 |
119 |
120 | 121 |

122 | 123 | 130 | 131 |

132 |
133 | 134 |
135 | 136 |

137 | 138 | 145 | 146 |

147 |
148 | 149 |
150 | 151 |

152 | 153 | 160 | 161 |

162 |
163 |
164 |

165 | 173 |

174 |
175 |
176 |

177 | 185 |

186 |
187 |
188 |

189 | 197 |

198 |
199 |
200 |

201 | 209 |

210 |
211 |
212 |

213 | 221 |

222 |
223 |
224 |

225 | 233 |

234 |
235 |
236 |

237 | 245 |

246 |
247 |
248 |
249 |

Editor

250 | 272 |
273 |
274 |

Code

275 | 297 | `} 298 | /> 299 |
300 |
301 | ); 302 | } 303 | } 304 | 305 | render(, document.getElementById('example')); 306 | -------------------------------------------------------------------------------- /example/split.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Split Editor 6 | 7 | 8 | 9 |
10 |
11 |

React-Ace: Split Editor Example

12 |
13 |
14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /example/split.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import SplitAceEditor from '../src/split'; 4 | import '../webpack-resolver-min'; 5 | import 'ace-builds/src-noconflict/ext-language_tools'; 6 | 7 | const languages = [ 8 | 'javascript', 9 | 'java', 10 | 'python', 11 | 'xml', 12 | 'ruby', 13 | 'sass', 14 | 'markdown', 15 | 'mysql', 16 | 'json', 17 | 'html', 18 | 'handlebars', 19 | 'golang', 20 | 'csharp', 21 | 'elixir', 22 | 'typescript', 23 | 'css', 24 | ]; 25 | 26 | languages.forEach((lang) => { 27 | ace.config.setModuleUrl( 28 | `ace/snippets/${lang}`, 29 | require(`file-loader!ace-builds/src-min-noconflict/snippets/${lang}`), 30 | ); 31 | }); 32 | 33 | const themes = [ 34 | 'monokai', 35 | 'github', 36 | 'tomorrow', 37 | 'kuroir', 38 | 'twilight', 39 | 'xcode', 40 | 'textmate', 41 | 'solarized_dark', 42 | 'solarized_light', 43 | 'terminal', 44 | ]; 45 | 46 | const defaultValue = [ 47 | `function onLoad(editor) { 48 | console.log("i've loaded"); 49 | }`, 50 | 'const secondInput = "me i am the second input";', 51 | ]; 52 | class App extends Component { 53 | onLoad() { 54 | console.log("i've loaded"); 55 | } 56 | onChange(newValue) { 57 | console.log('change', newValue); 58 | this.setState({ 59 | value: newValue, 60 | }); 61 | } 62 | 63 | onSelectionChange(newValue, event) { 64 | console.log('select-change', newValue); 65 | console.log('select-change-event', event); 66 | } 67 | 68 | onCursorChange(newValue, event) { 69 | console.log('cursor-change', newValue); 70 | console.log('cursor-change-event', event); 71 | } 72 | 73 | setTheme(e) { 74 | this.setState({ 75 | theme: e.target.value, 76 | }); 77 | } 78 | setMode(e) { 79 | this.setState({ 80 | mode: e.target.value, 81 | }); 82 | } 83 | setBoolean(name, value) { 84 | this.setState({ 85 | [name]: value, 86 | }); 87 | } 88 | setFontSize(e) { 89 | this.setState({ 90 | fontSize: parseInt(e.target.value, 10), 91 | }); 92 | } 93 | setSplits(e) { 94 | this.setState({ 95 | splits: parseInt(e.target.value, 10), 96 | }); 97 | } 98 | setOrientation(e) { 99 | this.setState({ 100 | orientation: e.target.value, 101 | }); 102 | } 103 | constructor(props) { 104 | super(props); 105 | this.state = { 106 | splits: 2, 107 | orientation: 'beside', 108 | value: defaultValue, 109 | theme: 'github', 110 | mode: 'javascript', 111 | enableBasicAutocompletion: false, 112 | enableLiveAutocompletion: false, 113 | fontSize: 14, 114 | showGutter: true, 115 | showPrintMargin: true, 116 | highlightActiveLine: true, 117 | enableSnippets: false, 118 | showLineNumbers: true, 119 | }; 120 | this.setTheme = this.setTheme.bind(this); 121 | this.setMode = this.setMode.bind(this); 122 | this.onChange = this.onChange.bind(this); 123 | this.setFontSize = this.setFontSize.bind(this); 124 | this.setBoolean = this.setBoolean.bind(this); 125 | this.setSplits = this.setSplits.bind(this); 126 | this.setOrientation = this.setOrientation.bind(this); 127 | } 128 | render() { 129 | return ( 130 |
131 |
132 |
133 | 134 |

135 | 136 | 143 | 144 |

145 |
146 | 147 |
148 | 149 |

150 | 151 | 158 | 159 |

160 |
161 | 162 |
163 | 164 |

165 | 166 | 173 | 174 |

175 |
176 | 177 |
178 | 179 |

180 | 181 | 188 | 189 |

190 |
191 | 192 |
193 | 194 |

195 | 196 | 203 | 204 |

205 |
206 |
207 |

208 | 216 |

217 |
218 |
219 |

220 | 228 |

229 |
230 |
231 |

232 | 240 |

241 |
242 |
243 |

244 | 252 |

253 |
254 |
255 |

256 | 264 |

265 |
266 |
267 |

268 | 276 |

277 |
278 |
279 |

280 | 288 |

289 |
290 |
291 |
292 |

Editor

293 | 319 |
320 |
321 | ); 322 | } 323 | } 324 | 325 | render(, document.getElementById('example')); 326 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manubb/react-ace-builds/655d21439df44511570320486bee6430b7ee3d34/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-ace-builds", 3 | "version": "7.4.1", 4 | "description": "A react component for Ace Editor", 5 | "main": "lib/index.js", 6 | "types": "types.d.ts", 7 | "scripts": { 8 | "prettier": "prettier --print-width 120 --single-quote --trailing-comma all --write \"src/**\" \"example/*.js\"", 9 | "clean": "rimraf lib", 10 | "lint": "node_modules/.bin/eslint src/*", 11 | "example": "webpack serve --config webpack.config.example.js", 12 | "build:example": "webpack --config webpack.config.example.js --mode=production", 13 | "build": "babel src --out-dir lib", 14 | "check": "npm run lint", 15 | "preversion": "npm run clean && npm run check", 16 | "version": "npm run build", 17 | "postversion": "git push && git push --tags && npm run clean", 18 | "prepublishOnly": "npm run clean && npm run build", 19 | "test": "mocha --require @babel/register --require tests/setup.js tests/**/*.spec.js --exit", 20 | "coverage": "nyc npm run test", 21 | "prepare": "husky install" 22 | }, 23 | "author": "James Hrisho", 24 | "contributors": [ 25 | { 26 | "name": "Manuel Baclet", 27 | "email": "mbaclet@gmail.com", 28 | "url": "https://github.com/manubb" 29 | }, 30 | { 31 | "name": "Dennis Oelkers", 32 | "email": "dennis@graylog.com", 33 | "url": "https://github.com/dennisoelkers" 34 | } 35 | ], 36 | "license": "MIT", 37 | "devDependencies": { 38 | "@babel/cli": "^7.13.16", 39 | "@babel/core": "^7.14.2", 40 | "@babel/eslint-parser": "^7.14.2", 41 | "@babel/preset-env": "^7.14.2", 42 | "@babel/preset-react": "^7.13.13", 43 | "@babel/register": "^7.13.16", 44 | "@wojtekmaj/enzyme-adapter-react-17": "^0.6.1", 45 | "babel-loader": "^9.1.2", 46 | "chai": "^4.3.4", 47 | "coveralls": "^3.1.0", 48 | "enzyme": "^3.11.0", 49 | "eslint": "^8.34.0", 50 | "eslint-plugin-import": "^2.22.1", 51 | "eslint-plugin-jsx-a11y": "^6.4.1", 52 | "eslint-plugin-react": "^7.23.2", 53 | "file-loader": "^6.2.0", 54 | "husky": "^8.0.3", 55 | "jsdom": "^21.1.0", 56 | "mocha": "^10.2.0", 57 | "nyc": "^15.1.0", 58 | "prettier": "^2.3.0", 59 | "pretty-quick": "^3.1.0", 60 | "prop-types": "^15.7.2", 61 | "react": "^17.0.2", 62 | "react-dom": "^17.0.2", 63 | "react-test-renderer": "^17.0.2", 64 | "rimraf": "^4.1.2", 65 | "sinon": "^15.0.1", 66 | "webpack": "^5.37.0", 67 | "webpack-cli": "^5.0.1", 68 | "webpack-dev-server": "^4.11.1" 69 | }, 70 | "keywords": [ 71 | "ace", 72 | "ace editor", 73 | "react-component", 74 | "react", 75 | "ace-builds" 76 | ], 77 | "dependencies": { 78 | "ace-builds": "1.31.1", 79 | "diff-match-patch": "^1.0.5", 80 | "lodash.get": "^4.4.2", 81 | "lodash.isequal": "^4.5.0" 82 | }, 83 | "peerDependencies": { 84 | "react": "^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0", 85 | "react-dom": "^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0" 86 | }, 87 | "nyc": { 88 | "exclude": [ 89 | "**/*.spec.js", 90 | "**/setup.js", 91 | "node_modules" 92 | ], 93 | "extension": [ 94 | ".js", 95 | ".jsx" 96 | ], 97 | "reporter": [ 98 | "lcov", 99 | "text-lcov", 100 | "text", 101 | "html" 102 | ] 103 | }, 104 | "repository": { 105 | "type": "git", 106 | "url": "http://github.com/manubb/react-ace-builds.git" 107 | }, 108 | "bugs": "https://github.com/manubb/react-ace-builds/issues", 109 | "homepage": "https://github.com/manubb/react-ace-builds#readme" 110 | } 111 | -------------------------------------------------------------------------------- /src/ace.js: -------------------------------------------------------------------------------- 1 | import ace, { Range } from 'ace-builds'; 2 | import React, { Component } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import isEqual from 'lodash.isequal'; 5 | 6 | import { editorOptions, editorEvents, debounce } from './editorOptions.js'; 7 | 8 | export default class ReactAce extends Component { 9 | constructor(props) { 10 | super(props); 11 | editorEvents.forEach((method) => { 12 | this[method] = this[method].bind(this); 13 | }); 14 | this.debounce = debounce; 15 | } 16 | 17 | componentDidMount() { 18 | const { 19 | className, 20 | onBeforeLoad, 21 | onValidate, 22 | mode, 23 | focus, 24 | theme, 25 | fontSize, 26 | value, 27 | defaultValue, 28 | cursorStart, 29 | showGutter, 30 | wrapEnabled, 31 | showPrintMargin, 32 | scrollMargin = [0, 0, 0, 0], 33 | keyboardHandler, 34 | onLoad, 35 | commands, 36 | annotations, 37 | markers, 38 | } = this.props; 39 | 40 | this.editor = ace.edit(this.refEditor); 41 | 42 | if (onBeforeLoad) { 43 | onBeforeLoad(ace); 44 | } 45 | 46 | const editorProps = Object.keys(this.props.editorProps); 47 | for (let i = 0; i < editorProps.length; i++) { 48 | this.editor[editorProps[i]] = this.props.editorProps[editorProps[i]]; 49 | } 50 | if (this.props.debounceChangePeriod) { 51 | this.onChange = this.debounce(this.onChange, this.props.debounceChangePeriod); 52 | } 53 | this.editor.renderer.setScrollMargin(scrollMargin[0], scrollMargin[1], scrollMargin[2], scrollMargin[3]); 54 | this.editor.getSession().setMode(`ace/mode/${mode}`); 55 | this.editor.setTheme(`ace/theme/${theme}`); 56 | this.editor.setFontSize(fontSize); 57 | this.editor.getSession().setValue(!defaultValue ? value : defaultValue, cursorStart); 58 | this.editor.navigateFileEnd(); 59 | this.editor.renderer.setShowGutter(showGutter); 60 | this.editor.getSession().setUseWrapMode(wrapEnabled); 61 | this.editor.setShowPrintMargin(showPrintMargin); 62 | this.editor.on('focus', this.onFocus); 63 | this.editor.on('blur', this.onBlur); 64 | this.editor.on('copy', this.onCopy); 65 | this.editor.on('paste', this.onPaste); 66 | this.editor.on('change', this.onChange); 67 | this.editor.on('input', this.onInput); 68 | this.editor.getSession().selection.on('changeSelection', this.onSelectionChange); 69 | this.editor.getSession().selection.on('changeCursor', this.onCursorChange); 70 | if (onValidate) { 71 | this.editor.getSession().on('changeAnnotation', () => { 72 | const annotations = this.editor.getSession().getAnnotations(); 73 | this.props.onValidate(annotations); 74 | }); 75 | } 76 | this.editor.session.on('changeScrollTop', this.onScroll); 77 | this.editor.getSession().setAnnotations(annotations || []); 78 | if (markers && markers.length > 0) { 79 | this.handleMarkers(markers); 80 | } 81 | 82 | // get a list of possible options to avoid 'misspelled option errors' 83 | const availableOptions = this.editor.$options; 84 | for (let i = 0; i < editorOptions.length; i++) { 85 | const option = editorOptions[i]; 86 | if (Object.prototype.hasOwnProperty.call(availableOptions, option)) { 87 | this.editor.setOption(option, this.props[option]); 88 | } else if (this.props[option]) { 89 | console.warn( 90 | `ReactAce: editor option ${option} was activated but not found. Did you need to import a related tool or did you possibly mispell the option?`, 91 | ); 92 | } 93 | } 94 | this.handleOptions(this.props); 95 | 96 | if (Array.isArray(commands)) { 97 | commands.forEach((command) => { 98 | if (typeof command.exec == 'string') { 99 | this.editor.commands.bindKey(command.bindKey, command.exec); 100 | } else { 101 | this.editor.commands.addCommand(command); 102 | } 103 | }); 104 | } 105 | 106 | if (keyboardHandler) { 107 | this.editor.setKeyboardHandler('ace/keyboard/' + keyboardHandler); 108 | } 109 | 110 | if (className) { 111 | this.refEditor.className += ' ' + className; 112 | } 113 | 114 | if (onLoad) { 115 | onLoad(this.editor); 116 | } 117 | 118 | this.editor.resize(); 119 | 120 | if (focus) { 121 | this.editor.focus(); 122 | } 123 | } 124 | 125 | componentDidUpdate(prevProps) { 126 | const oldProps = prevProps; 127 | const nextProps = this.props; 128 | 129 | for (let i = 0; i < editorOptions.length; i++) { 130 | const option = editorOptions[i]; 131 | if (nextProps[option] !== oldProps[option]) { 132 | this.editor.setOption(option, nextProps[option]); 133 | } 134 | } 135 | 136 | if (nextProps.className !== oldProps.className) { 137 | let appliedClasses = this.refEditor.className; 138 | let appliedClassesArray = appliedClasses.trim().split(' '); 139 | let oldClassesArray = oldProps.className.trim().split(' '); 140 | oldClassesArray.forEach((oldClass) => { 141 | let index = appliedClassesArray.indexOf(oldClass); 142 | appliedClassesArray.splice(index, 1); 143 | }); 144 | this.refEditor.className = ' ' + nextProps.className + ' ' + appliedClassesArray.join(' '); 145 | } 146 | 147 | // First process editor value, as it may create a new session (see issue #300) 148 | if (this.editor && this.editor.getValue() !== nextProps.value) { 149 | // editor.setValue is a synchronous function call, change event is emitted before setValue return. 150 | this.silent = true; 151 | const pos = this.editor.session.selection.toJSON(); 152 | this.editor.setValue(nextProps.value, nextProps.cursorStart); 153 | this.editor.session.selection.fromJSON(pos); 154 | this.silent = false; 155 | } 156 | 157 | if (nextProps.mode !== oldProps.mode) { 158 | this.editor.getSession().setMode('ace/mode/' + nextProps.mode); 159 | } 160 | if (nextProps.theme !== oldProps.theme) { 161 | this.editor.setTheme('ace/theme/' + nextProps.theme); 162 | } 163 | if (nextProps.keyboardHandler !== oldProps.keyboardHandler) { 164 | if (nextProps.keyboardHandler) { 165 | this.editor.setKeyboardHandler('ace/keyboard/' + nextProps.keyboardHandler); 166 | } else { 167 | this.editor.setKeyboardHandler(null); 168 | } 169 | } 170 | if (nextProps.fontSize !== oldProps.fontSize) { 171 | this.editor.setFontSize(nextProps.fontSize); 172 | } 173 | if (nextProps.wrapEnabled !== oldProps.wrapEnabled) { 174 | this.editor.getSession().setUseWrapMode(nextProps.wrapEnabled); 175 | } 176 | if (nextProps.showPrintMargin !== oldProps.showPrintMargin) { 177 | this.editor.setShowPrintMargin(nextProps.showPrintMargin); 178 | } 179 | if (nextProps.showGutter !== oldProps.showGutter) { 180 | this.editor.renderer.setShowGutter(nextProps.showGutter); 181 | } 182 | if (!isEqual(nextProps.setOptions, oldProps.setOptions)) { 183 | this.handleOptions(nextProps); 184 | } 185 | if (!isEqual(nextProps.annotations, oldProps.annotations)) { 186 | this.editor.getSession().setAnnotations(nextProps.annotations || []); 187 | } 188 | if (!isEqual(nextProps.markers, oldProps.markers) && Array.isArray(nextProps.markers)) { 189 | this.handleMarkers(nextProps.markers); 190 | } 191 | 192 | // this doesn't look like it works at all.... 193 | if (!isEqual(nextProps.scrollMargin, oldProps.scrollMargin)) { 194 | this.handleScrollMargins(nextProps.scrollMargin); 195 | } 196 | 197 | if (prevProps.height !== this.props.height || prevProps.width !== this.props.width) { 198 | this.editor.resize(); 199 | } 200 | if (this.props.focus && !prevProps.focus) { 201 | this.editor.focus(); 202 | } 203 | } 204 | 205 | handleScrollMargins(margins = [0, 0, 0, 0]) { 206 | this.editor.renderer.setScrollMargins(margins[0], margins[1], margins[2], margins[3]); 207 | } 208 | 209 | componentWillUnmount() { 210 | this.editor.destroy(); 211 | this.editor = null; 212 | } 213 | 214 | onChange(event) { 215 | if (this.props.onChange && !this.silent) { 216 | const value = this.editor.getValue(); 217 | this.props.onChange(value, event); 218 | } 219 | } 220 | 221 | onSelectionChange(event) { 222 | if (this.props.onSelectionChange) { 223 | const value = this.editor.getSelection(); 224 | this.props.onSelectionChange(value, event); 225 | } 226 | } 227 | onCursorChange(event) { 228 | if (this.props.onCursorChange) { 229 | const value = this.editor.getSelection(); 230 | this.props.onCursorChange(value, event); 231 | } 232 | } 233 | onInput(event) { 234 | if (this.props.onInput) { 235 | this.props.onInput(event); 236 | } 237 | } 238 | onFocus(event) { 239 | if (this.props.onFocus) { 240 | this.props.onFocus(event, this.editor); 241 | } 242 | } 243 | 244 | onBlur(event) { 245 | if (this.props.onBlur) { 246 | this.props.onBlur(event, this.editor); 247 | } 248 | } 249 | 250 | onCopy(text) { 251 | if (this.props.onCopy) { 252 | this.props.onCopy(text); 253 | } 254 | } 255 | 256 | onPaste(text) { 257 | if (this.props.onPaste) { 258 | this.props.onPaste(text); 259 | } 260 | } 261 | 262 | onScroll() { 263 | if (this.props.onScroll) { 264 | this.props.onScroll(this.editor); 265 | } 266 | } 267 | 268 | handleOptions(props) { 269 | const setOptions = Object.keys(props.setOptions); 270 | for (let y = 0; y < setOptions.length; y++) { 271 | this.editor.setOption(setOptions[y], props.setOptions[setOptions[y]]); 272 | } 273 | } 274 | 275 | handleMarkers(markers) { 276 | // remove foreground markers 277 | let currentMarkers = this.editor.getSession().getMarkers(true); 278 | for (const i in currentMarkers) { 279 | if (Object.prototype.hasOwnProperty.call(currentMarkers, i)) { 280 | this.editor.getSession().removeMarker(currentMarkers[i].id); 281 | } 282 | } 283 | // remove background markers 284 | currentMarkers = this.editor.getSession().getMarkers(false); 285 | for (const i in currentMarkers) { 286 | if (Object.prototype.hasOwnProperty.call(currentMarkers, i)) { 287 | this.editor.getSession().removeMarker(currentMarkers[i].id); 288 | } 289 | } 290 | // add new markers 291 | markers.forEach(({ startRow, startCol, endRow, endCol, className, type, inFront = false }) => { 292 | const range = new Range(startRow, startCol, endRow, endCol); 293 | this.editor.getSession().addMarker(range, className, type, inFront); 294 | }); 295 | } 296 | 297 | updateRef(item) { 298 | this.refEditor = item; 299 | } 300 | 301 | render() { 302 | const { name, width, height, style } = this.props; 303 | const divStyle = { width, height, ...style }; 304 | return
; 305 | } 306 | } 307 | 308 | ReactAce.propTypes = { 309 | mode: PropTypes.string, 310 | focus: PropTypes.bool, 311 | theme: PropTypes.string, 312 | name: PropTypes.string, 313 | className: PropTypes.string, 314 | height: PropTypes.string, 315 | width: PropTypes.string, 316 | fontSize: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), 317 | showGutter: PropTypes.bool, 318 | onChange: PropTypes.func, 319 | onCopy: PropTypes.func, 320 | onPaste: PropTypes.func, 321 | onFocus: PropTypes.func, 322 | onInput: PropTypes.func, 323 | onBlur: PropTypes.func, 324 | onScroll: PropTypes.func, 325 | value: PropTypes.string, 326 | defaultValue: PropTypes.string, 327 | onLoad: PropTypes.func, 328 | onSelectionChange: PropTypes.func, 329 | onCursorChange: PropTypes.func, 330 | onBeforeLoad: PropTypes.func, 331 | onValidate: PropTypes.func, 332 | minLines: PropTypes.number, 333 | maxLines: PropTypes.number, 334 | readOnly: PropTypes.bool, 335 | highlightActiveLine: PropTypes.bool, 336 | tabSize: PropTypes.number, 337 | showPrintMargin: PropTypes.bool, 338 | cursorStart: PropTypes.number, 339 | debounceChangePeriod: PropTypes.number, 340 | editorProps: PropTypes.object, 341 | setOptions: PropTypes.object, 342 | style: PropTypes.object, 343 | scrollMargin: PropTypes.array, 344 | annotations: PropTypes.array, 345 | markers: PropTypes.array, 346 | keyboardHandler: PropTypes.string, 347 | wrapEnabled: PropTypes.bool, 348 | enableBasicAutocompletion: PropTypes.oneOfType([PropTypes.bool, PropTypes.array]), 349 | enableLiveAutocompletion: PropTypes.oneOfType([PropTypes.bool, PropTypes.array]), 350 | commands: PropTypes.array, 351 | }; 352 | 353 | ReactAce.defaultProps = { 354 | name: 'ace-editor', 355 | focus: false, 356 | mode: '', 357 | theme: '', 358 | height: '500px', 359 | width: '500px', 360 | value: '', 361 | fontSize: 12, 362 | showGutter: true, 363 | onChange: null, 364 | onPaste: null, 365 | onLoad: null, 366 | onScroll: null, 367 | minLines: null, 368 | maxLines: null, 369 | readOnly: false, 370 | highlightActiveLine: true, 371 | showPrintMargin: true, 372 | tabSize: 4, 373 | cursorStart: 1, 374 | editorProps: {}, 375 | style: {}, 376 | scrollMargin: [0, 0, 0, 0], 377 | setOptions: {}, 378 | wrapEnabled: false, 379 | enableBasicAutocompletion: false, 380 | enableLiveAutocompletion: false, 381 | }; 382 | -------------------------------------------------------------------------------- /src/diff.js: -------------------------------------------------------------------------------- 1 | import SplitEditor from './split.js'; 2 | import React, { Component } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import DiffMatchPatch from 'diff-match-patch'; 5 | 6 | export default class DiffComponent extends Component { 7 | diff() { 8 | const dmp = new DiffMatchPatch(); 9 | const lhString = this.props.value[0]; 10 | const rhString = this.props.value[1]; 11 | 12 | if (lhString.length === 0 && rhString.length === 0) { 13 | return []; 14 | } 15 | 16 | const diff = dmp.diff_main(lhString, rhString); 17 | dmp.diff_cleanupSemantic(diff); 18 | 19 | const diffedLines = this.generateDiffedLines(diff); 20 | const codeEditorSettings = this.setCodeMarkers(diffedLines); 21 | return codeEditorSettings; 22 | } 23 | 24 | generateDiffedLines(diff) { 25 | const C = { 26 | DIFF_EQUAL: 0, 27 | DIFF_DELETE: -1, 28 | DIFF_INSERT: 1, 29 | }; 30 | 31 | const diffedLines = { 32 | left: [], 33 | right: [], 34 | }; 35 | 36 | const cursor = { 37 | left: 1, 38 | right: 1, 39 | }; 40 | 41 | diff.forEach((chunk) => { 42 | const chunkType = chunk[0]; 43 | const text = chunk[1]; 44 | let lines = text.split('\n').length - 1; 45 | 46 | // diff-match-patch sometimes returns empty strings at random 47 | if (text.length === 0) { 48 | return; 49 | } 50 | 51 | const firstChar = text[0]; 52 | const lastChar = text[text.length - 1]; 53 | let linesToHighlight = 0; 54 | 55 | switch (chunkType) { 56 | case C.DIFF_EQUAL: 57 | cursor.left += lines; 58 | cursor.right += lines; 59 | 60 | break; 61 | case C.DIFF_DELETE: 62 | // If the deletion starts with a newline, push the cursor down to that line 63 | if (firstChar === '\n') { 64 | cursor.left++; 65 | lines--; 66 | } 67 | 68 | linesToHighlight = lines; 69 | 70 | // If the deletion does not include a newline, highlight the same line on the right 71 | if (linesToHighlight === 0) { 72 | diffedLines.right.push({ 73 | startLine: cursor.right, 74 | endLine: cursor.right, 75 | }); 76 | } 77 | 78 | // If the last character is a newline, we don't want to highlight that line 79 | if (lastChar === '\n') { 80 | linesToHighlight -= 1; 81 | } 82 | 83 | diffedLines.left.push({ 84 | startLine: cursor.left, 85 | endLine: cursor.left + linesToHighlight, 86 | }); 87 | 88 | cursor.left += lines; 89 | break; 90 | case C.DIFF_INSERT: 91 | // If the insertion starts with a newline, push the cursor down to that line 92 | if (firstChar === '\n') { 93 | cursor.right++; 94 | lines--; 95 | } 96 | 97 | linesToHighlight = lines; 98 | 99 | // If the insertion does not include a newline, highlight the same line on the left 100 | if (linesToHighlight === 0) { 101 | diffedLines.left.push({ 102 | startLine: cursor.left, 103 | endLine: cursor.left, 104 | }); 105 | } 106 | 107 | // If the last character is a newline, we don't want to highlight that line 108 | if (lastChar === '\n') { 109 | linesToHighlight -= 1; 110 | } 111 | 112 | diffedLines.right.push({ 113 | startLine: cursor.right, 114 | endLine: cursor.right + linesToHighlight, 115 | }); 116 | 117 | cursor.right += lines; 118 | break; 119 | default: 120 | throw new Error('Diff type was not defined.'); 121 | } 122 | }); 123 | return diffedLines; 124 | } 125 | 126 | // Receives a collection of line numbers and iterates through them to highlight appropriately 127 | // Returns an object that tells the render() method how to display the code editors 128 | setCodeMarkers(diffedLines = { left: [], right: [] }) { 129 | const codeEditorSettings = []; 130 | 131 | const newMarkerSet = { 132 | left: [], 133 | right: [], 134 | }; 135 | 136 | for (let i = 0; i < diffedLines.left.length; i++) { 137 | let markerObj = { 138 | startRow: diffedLines.left[i].startLine - 1, 139 | endRow: diffedLines.left[i].endLine, 140 | type: 'text', 141 | className: 'codeMarker', 142 | }; 143 | newMarkerSet.left.push(markerObj); 144 | } 145 | 146 | for (let i = 0; i < diffedLines.right.length; i++) { 147 | let markerObj = { 148 | startRow: diffedLines.right[i].startLine - 1, 149 | endRow: diffedLines.right[i].endLine, 150 | type: 'text', 151 | className: 'codeMarker', 152 | }; 153 | newMarkerSet.right.push(markerObj); 154 | } 155 | 156 | codeEditorSettings[0] = newMarkerSet.left; 157 | codeEditorSettings[1] = newMarkerSet.right; 158 | 159 | return codeEditorSettings; 160 | } 161 | 162 | render() { 163 | const markers = this.diff(); 164 | return ( 165 | 198 | ); 199 | } 200 | } 201 | 202 | DiffComponent.propTypes = { 203 | cursorStart: PropTypes.number, 204 | editorProps: PropTypes.object, 205 | enableBasicAutocompletion: PropTypes.bool, 206 | enableLiveAutocompletion: PropTypes.bool, 207 | focus: PropTypes.bool, 208 | fontSize: PropTypes.number, 209 | height: PropTypes.string, 210 | highlightActiveLine: PropTypes.bool, 211 | maxLines: PropTypes.func, 212 | minLines: PropTypes.func, 213 | mode: PropTypes.string, 214 | name: PropTypes.string, 215 | className: PropTypes.string, 216 | onLoad: PropTypes.func, 217 | onPaste: PropTypes.func, 218 | onScroll: PropTypes.func, 219 | onChange: PropTypes.func, 220 | orientation: PropTypes.string, 221 | readOnly: PropTypes.bool, 222 | scrollMargin: PropTypes.array, 223 | setOptions: PropTypes.object, 224 | showGutter: PropTypes.bool, 225 | showPrintMargin: PropTypes.bool, 226 | splits: PropTypes.number, 227 | style: PropTypes.object, 228 | tabSize: PropTypes.number, 229 | theme: PropTypes.string, 230 | value: PropTypes.array, 231 | width: PropTypes.string, 232 | wrapEnabled: PropTypes.bool, 233 | }; 234 | 235 | DiffComponent.defaultProps = { 236 | cursorStart: 1, 237 | editorProps: {}, 238 | enableBasicAutocompletion: false, 239 | enableLiveAutocompletion: false, 240 | focus: false, 241 | fontSize: 12, 242 | height: '500px', 243 | highlightActiveLine: true, 244 | maxLines: null, 245 | minLines: null, 246 | mode: '', 247 | name: 'ace-editor', 248 | onLoad: null, 249 | onScroll: null, 250 | onPaste: null, 251 | onChange: null, 252 | orientation: 'beside', 253 | readOnly: false, 254 | scrollMargin: [0, 0, 0, 0], 255 | setOptions: {}, 256 | showGutter: true, 257 | showPrintMargin: true, 258 | splits: 2, 259 | style: {}, 260 | tabSize: 4, 261 | theme: 'github', 262 | value: ['', ''], 263 | width: '500px', 264 | wrapEnabled: true, 265 | }; 266 | -------------------------------------------------------------------------------- /src/editorOptions.js: -------------------------------------------------------------------------------- 1 | const editorOptions = [ 2 | 'minLines', 3 | 'maxLines', 4 | 'readOnly', 5 | 'highlightActiveLine', 6 | 'tabSize', 7 | 'enableBasicAutocompletion', 8 | 'enableLiveAutocompletion', 9 | 'enableSnippets', 10 | ]; 11 | 12 | const editorEvents = [ 13 | 'onChange', 14 | 'onFocus', 15 | 'onInput', 16 | 'onBlur', 17 | 'onCopy', 18 | 'onPaste', 19 | 'onSelectionChange', 20 | 'onCursorChange', 21 | 'onScroll', 22 | 'handleOptions', 23 | 'updateRef', 24 | ]; 25 | const debounce = (fn, delay) => { 26 | var timer = null; 27 | return function () { 28 | var context = this, 29 | args = arguments; 30 | clearTimeout(timer); 31 | timer = setTimeout(function () { 32 | fn.apply(context, args); 33 | }, delay); 34 | }; 35 | }; 36 | export { editorOptions, editorEvents, debounce }; 37 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import ace from './ace.js'; 2 | import split from './split.js'; 3 | import diff from './diff.js'; 4 | export { split, diff }; 5 | export default ace; 6 | -------------------------------------------------------------------------------- /src/split.js: -------------------------------------------------------------------------------- 1 | import ace from 'ace-builds'; 2 | import { Range, UndoManager } from 'ace-builds'; 3 | import { Split } from 'ace-builds/src-noconflict/ext-split'; 4 | import React, { Component } from 'react'; 5 | import PropTypes from 'prop-types'; 6 | import isEqual from 'lodash.isequal'; 7 | import get from 'lodash.get'; 8 | 9 | import { editorOptions, editorEvents, debounce } from './editorOptions.js'; 10 | 11 | export default class SplitComponent extends Component { 12 | constructor(props) { 13 | super(props); 14 | editorEvents.forEach((method) => { 15 | this[method] = this[method].bind(this); 16 | }); 17 | this.debounce = debounce; 18 | } 19 | 20 | componentDidMount() { 21 | const { 22 | className, 23 | onBeforeLoad, 24 | mode, 25 | focus, 26 | theme, 27 | fontSize, 28 | value, 29 | defaultValue, 30 | cursorStart, 31 | showGutter, 32 | wrapEnabled, 33 | showPrintMargin, 34 | scrollMargin = [0, 0, 0, 0], 35 | keyboardHandler, 36 | onLoad, 37 | commands, 38 | annotations, 39 | markers, 40 | splits, 41 | } = this.props; 42 | 43 | this.editor = ace.edit(this.refEditor); 44 | 45 | if (onBeforeLoad) { 46 | onBeforeLoad(ace); 47 | } 48 | 49 | const editorProps = Object.keys(this.props.editorProps); 50 | 51 | var split = new Split(this.editor.container, `ace/theme/${theme}`, splits); 52 | this.editor.env.split = split; 53 | 54 | this.splitEditor = split.getEditor(0); 55 | this.split = split; 56 | // in a split scenario we don't want a print margin for the entire application 57 | this.editor.setShowPrintMargin(false); 58 | this.editor.renderer.setShowGutter(false); 59 | // get a list of possible options to avoid 'misspelled option errors' 60 | const availableOptions = this.splitEditor.$options; 61 | if (this.props.debounceChangePeriod) { 62 | this.onChange = this.debounce(this.onChange, this.props.debounceChangePeriod); 63 | } 64 | split.forEach((editor, index) => { 65 | for (let i = 0; i < editorProps.length; i++) { 66 | editor[editorProps[i]] = this.props.editorProps[editorProps[i]]; 67 | } 68 | const defaultValueForEditor = get(defaultValue, index); 69 | const valueForEditor = get(value, index, ''); 70 | editor.session.setUndoManager(new UndoManager()); 71 | editor.setTheme(`ace/theme/${theme}`); 72 | editor.renderer.setScrollMargin(scrollMargin[0], scrollMargin[1], scrollMargin[2], scrollMargin[3]); 73 | editor.getSession().setMode(`ace/mode/${mode}`); 74 | editor.setFontSize(fontSize); 75 | editor.renderer.setShowGutter(showGutter); 76 | editor.getSession().setUseWrapMode(wrapEnabled); 77 | editor.setShowPrintMargin(showPrintMargin); 78 | editor.on('focus', this.onFocus); 79 | editor.on('blur', this.onBlur); 80 | editor.on('input', this.onInput); 81 | editor.on('copy', this.onCopy); 82 | editor.on('paste', this.onPaste); 83 | editor.on('change', this.onChange); 84 | editor.getSession().selection.on('changeSelection', this.onSelectionChange); 85 | editor.getSession().selection.on('changeCursor', this.onCursorChange); 86 | editor.session.on('changeScrollTop', this.onScroll); 87 | editor.setValue(defaultValueForEditor === undefined ? valueForEditor : defaultValueForEditor, cursorStart); 88 | const newAnnotations = get(annotations, index, []); 89 | const newMarkers = get(markers, index, []); 90 | editor.getSession().setAnnotations(newAnnotations); 91 | if (newMarkers && newMarkers.length > 0) { 92 | this.handleMarkers(newMarkers, editor); 93 | } 94 | 95 | for (let i = 0; i < editorOptions.length; i++) { 96 | const option = editorOptions[i]; 97 | if (Object.prototype.hasOwnProperty.call(availableOptions, option)) { 98 | editor.setOption(option, this.props[option]); 99 | } else if (this.props[option]) { 100 | console.warn( 101 | `ReaceAce: editor option ${option} was activated but not found. Did you need to import a related tool or did you possibly mispell the option?`, 102 | ); 103 | } 104 | } 105 | this.handleOptions(this.props, editor); 106 | 107 | if (Array.isArray(commands)) { 108 | commands.forEach((command) => { 109 | if (typeof command.exec == 'string') { 110 | editor.commands.bindKey(command.bindKey, command.exec); 111 | } else { 112 | editor.commands.addCommand(command); 113 | } 114 | }); 115 | } 116 | 117 | if (keyboardHandler) { 118 | editor.setKeyboardHandler('ace/keyboard/' + keyboardHandler); 119 | } 120 | }); 121 | 122 | if (className) { 123 | this.refEditor.className += ' ' + className; 124 | } 125 | 126 | if (focus) { 127 | this.splitEditor.focus(); 128 | } 129 | 130 | const sp = this.editor.env.split; 131 | sp.setOrientation(this.props.orientation === 'below' ? sp.BELOW : sp.BESIDE); 132 | sp.resize(true); 133 | if (onLoad) { 134 | onLoad(sp); 135 | } 136 | } 137 | 138 | componentDidUpdate(prevProps) { 139 | const oldProps = prevProps; 140 | const nextProps = this.props; 141 | 142 | const split = this.editor.env.split; 143 | 144 | if (nextProps.splits !== oldProps.splits) { 145 | split.setSplits(nextProps.splits); 146 | } 147 | 148 | if (nextProps.orientation !== oldProps.orientation) { 149 | split.setOrientation(nextProps.orientation === 'below' ? split.BELOW : split.BESIDE); 150 | } 151 | 152 | split.forEach((editor, index) => { 153 | if (nextProps.mode !== oldProps.mode) { 154 | editor.getSession().setMode('ace/mode/' + nextProps.mode); 155 | } 156 | if (nextProps.keyboardHandler !== oldProps.keyboardHandler) { 157 | if (nextProps.keyboardHandler) { 158 | editor.setKeyboardHandler('ace/keyboard/' + nextProps.keyboardHandler); 159 | } else { 160 | editor.setKeyboardHandler(null); 161 | } 162 | } 163 | if (nextProps.fontSize !== oldProps.fontSize) { 164 | editor.setFontSize(nextProps.fontSize); 165 | } 166 | if (nextProps.wrapEnabled !== oldProps.wrapEnabled) { 167 | editor.getSession().setUseWrapMode(nextProps.wrapEnabled); 168 | } 169 | if (nextProps.showPrintMargin !== oldProps.showPrintMargin) { 170 | editor.setShowPrintMargin(nextProps.showPrintMargin); 171 | } 172 | if (nextProps.showGutter !== oldProps.showGutter) { 173 | editor.renderer.setShowGutter(nextProps.showGutter); 174 | } 175 | 176 | for (let i = 0; i < editorOptions.length; i++) { 177 | const option = editorOptions[i]; 178 | if (nextProps[option] !== oldProps[option]) { 179 | editor.setOption(option, nextProps[option]); 180 | } 181 | } 182 | if (!isEqual(nextProps.setOptions, oldProps.setOptions)) { 183 | this.handleOptions(nextProps, editor); 184 | } 185 | const nextValue = get(nextProps.value, index, ''); 186 | if (editor.getValue() !== nextValue) { 187 | // editor.setValue is a synchronous function call, change event is emitted before setValue return. 188 | this.silent = true; 189 | const pos = editor.session.selection.toJSON(); 190 | editor.setValue(nextValue, nextProps.cursorStart); 191 | editor.session.selection.fromJSON(pos); 192 | this.silent = false; 193 | } 194 | const newAnnotations = get(nextProps.annotations, index, []); 195 | const oldAnnotations = get(oldProps.annotations, index, []); 196 | if (!isEqual(newAnnotations, oldAnnotations)) { 197 | editor.getSession().setAnnotations(newAnnotations); 198 | } 199 | 200 | const newMarkers = get(nextProps.markers, index, []); 201 | const oldMarkers = get(oldProps.markers, index, []); 202 | if (!isEqual(newMarkers, oldMarkers) && Array.isArray(newMarkers)) { 203 | this.handleMarkers(newMarkers, editor); 204 | } 205 | }); 206 | 207 | if (nextProps.className !== oldProps.className) { 208 | let appliedClasses = this.refEditor.className; 209 | let appliedClassesArray = appliedClasses.trim().split(' '); 210 | let oldClassesArray = oldProps.className.trim().split(' '); 211 | oldClassesArray.forEach((oldClass) => { 212 | let index = appliedClassesArray.indexOf(oldClass); 213 | appliedClassesArray.splice(index, 1); 214 | }); 215 | this.refEditor.className = ' ' + nextProps.className + ' ' + appliedClassesArray.join(' '); 216 | } 217 | 218 | if (nextProps.theme !== oldProps.theme) { 219 | split.setTheme('ace/theme/' + nextProps.theme); 220 | } 221 | 222 | if (nextProps.focus && !oldProps.focus) { 223 | this.splitEditor.focus(); 224 | } 225 | if (nextProps.height !== this.props.height || nextProps.width !== this.props.width) { 226 | this.editor.resize(); 227 | } 228 | } 229 | 230 | componentWillUnmount() { 231 | this.editor.destroy(); 232 | this.editor = null; 233 | } 234 | 235 | onChange(event) { 236 | if (this.props.onChange && !this.silent) { 237 | let value = []; 238 | this.editor.env.split.forEach((editor) => { 239 | value.push(editor.getValue()); 240 | }); 241 | this.props.onChange(value, event); 242 | } 243 | } 244 | 245 | onSelectionChange(event) { 246 | if (this.props.onSelectionChange) { 247 | let value = []; 248 | this.editor.env.split.forEach((editor) => { 249 | value.push(editor.getSelection()); 250 | }); 251 | this.props.onSelectionChange(value, event); 252 | } 253 | } 254 | onCursorChange(event) { 255 | if (this.props.onCursorChange) { 256 | let value = []; 257 | this.editor.env.split.forEach((editor) => { 258 | value.push(editor.getSelection()); 259 | }); 260 | this.props.onCursorChange(value, event); 261 | } 262 | } 263 | onFocus(event) { 264 | if (this.props.onFocus) { 265 | this.props.onFocus(event); 266 | } 267 | } 268 | 269 | onInput(event) { 270 | if (this.props.onInput) { 271 | this.props.onInput(event); 272 | } 273 | } 274 | 275 | onBlur(event) { 276 | if (this.props.onBlur) { 277 | this.props.onBlur(event); 278 | } 279 | } 280 | 281 | onCopy(text) { 282 | if (this.props.onCopy) { 283 | this.props.onCopy(text); 284 | } 285 | } 286 | 287 | onPaste(text) { 288 | if (this.props.onPaste) { 289 | this.props.onPaste(text); 290 | } 291 | } 292 | 293 | onScroll() { 294 | if (this.props.onScroll) { 295 | this.props.onScroll(this.editor); 296 | } 297 | } 298 | 299 | handleOptions(props, editor) { 300 | const setOptions = Object.keys(props.setOptions); 301 | for (let y = 0; y < setOptions.length; y++) { 302 | editor.setOption(setOptions[y], props.setOptions[setOptions[y]]); 303 | } 304 | } 305 | 306 | handleMarkers(markers, editor) { 307 | // remove foreground markers 308 | let currentMarkers = editor.getSession().getMarkers(true); 309 | for (const i in currentMarkers) { 310 | if (Object.prototype.hasOwnProperty.call(currentMarkers, i)) { 311 | editor.getSession().removeMarker(currentMarkers[i].id); 312 | } 313 | } 314 | // remove background markers 315 | currentMarkers = editor.getSession().getMarkers(false); 316 | for (const i in currentMarkers) { 317 | if (Object.prototype.hasOwnProperty.call(currentMarkers, i)) { 318 | editor.getSession().removeMarker(currentMarkers[i].id); 319 | } 320 | } 321 | // add new markers 322 | markers.forEach(({ startRow, startCol, endRow, endCol, className, type, inFront = false }) => { 323 | const range = new Range(startRow, startCol, endRow, endCol); 324 | editor.getSession().addMarker(range, className, type, inFront); 325 | }); 326 | } 327 | 328 | updateRef(item) { 329 | this.refEditor = item; 330 | } 331 | 332 | render() { 333 | const { name, width, height, style } = this.props; 334 | const divStyle = { width, height, ...style }; 335 | return
; 336 | } 337 | } 338 | 339 | SplitComponent.propTypes = { 340 | mode: PropTypes.string, 341 | splits: PropTypes.number, 342 | orientation: PropTypes.string, 343 | focus: PropTypes.bool, 344 | theme: PropTypes.string, 345 | name: PropTypes.string, 346 | className: PropTypes.string, 347 | height: PropTypes.string, 348 | width: PropTypes.string, 349 | fontSize: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), 350 | showGutter: PropTypes.bool, 351 | onChange: PropTypes.func, 352 | onCopy: PropTypes.func, 353 | onPaste: PropTypes.func, 354 | onFocus: PropTypes.func, 355 | onInput: PropTypes.func, 356 | onBlur: PropTypes.func, 357 | onScroll: PropTypes.func, 358 | value: PropTypes.arrayOf(PropTypes.string), 359 | defaultValue: PropTypes.arrayOf(PropTypes.string), 360 | debounceChangePeriod: PropTypes.number, 361 | onLoad: PropTypes.func, 362 | onSelectionChange: PropTypes.func, 363 | onCursorChange: PropTypes.func, 364 | onBeforeLoad: PropTypes.func, 365 | minLines: PropTypes.number, 366 | maxLines: PropTypes.number, 367 | readOnly: PropTypes.bool, 368 | highlightActiveLine: PropTypes.bool, 369 | tabSize: PropTypes.number, 370 | showPrintMargin: PropTypes.bool, 371 | cursorStart: PropTypes.number, 372 | editorProps: PropTypes.object, 373 | setOptions: PropTypes.object, 374 | style: PropTypes.object, 375 | scrollMargin: PropTypes.array, 376 | annotations: PropTypes.array, 377 | markers: PropTypes.array, 378 | keyboardHandler: PropTypes.string, 379 | wrapEnabled: PropTypes.bool, 380 | enableBasicAutocompletion: PropTypes.oneOfType([PropTypes.bool, PropTypes.array]), 381 | enableLiveAutocompletion: PropTypes.oneOfType([PropTypes.bool, PropTypes.array]), 382 | commands: PropTypes.array, 383 | }; 384 | 385 | SplitComponent.defaultProps = { 386 | name: 'ace-editor', 387 | focus: false, 388 | orientation: 'beside', 389 | splits: 2, 390 | mode: '', 391 | theme: '', 392 | height: '500px', 393 | width: '500px', 394 | value: [], 395 | fontSize: 12, 396 | showGutter: true, 397 | onChange: null, 398 | onPaste: null, 399 | onLoad: null, 400 | onScroll: null, 401 | minLines: null, 402 | maxLines: null, 403 | readOnly: false, 404 | highlightActiveLine: true, 405 | showPrintMargin: true, 406 | tabSize: 4, 407 | cursorStart: 1, 408 | editorProps: {}, 409 | style: {}, 410 | scrollMargin: [0, 0, 0, 0], 411 | setOptions: {}, 412 | wrapEnabled: false, 413 | enableBasicAutocompletion: false, 414 | enableLiveAutocompletion: false, 415 | }; 416 | -------------------------------------------------------------------------------- /tests/setup.js: -------------------------------------------------------------------------------- 1 | const { JSDOM } = require("jsdom"); 2 | 3 | const jsdom = new JSDOM(""); 4 | const { window } = jsdom; 5 | 6 | function copyProps(src, target) { 7 | Object.defineProperties(target, { 8 | ...Object.getOwnPropertyDescriptors(src), 9 | ...Object.getOwnPropertyDescriptors(target) 10 | }); 11 | } 12 | 13 | global.window = window; 14 | global.document = window.document; 15 | global.navigator = { 16 | userAgent: "node.js" 17 | }; 18 | global.requestAnimationFrame = function(callback) { 19 | return setTimeout(callback, 0); 20 | }; 21 | global.cancelAnimationFrame = function(id) { 22 | clearTimeout(id); 23 | }; 24 | copyProps(window, global); 25 | -------------------------------------------------------------------------------- /tests/src/ace.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import React from "react"; 3 | import sinon from "sinon"; 4 | import ace from "ace-builds"; 5 | import Enzyme, { mount } from "enzyme"; 6 | import AceEditor from "../../src/ace.js"; 7 | import Adapter from "@wojtekmaj/enzyme-adapter-react-17"; 8 | 9 | Enzyme.configure({ adapter: new Adapter() }); 10 | 11 | describe("Ace Component", () => { 12 | beforeEach(done => { 13 | const div = document.createElement("div"); 14 | window.domNode = div; 15 | document.body.appendChild(div); 16 | done(); 17 | }); 18 | 19 | afterEach(done => { 20 | document.body.removeChild(window.domNode); 21 | done(); 22 | }); 23 | 24 | const getMountOptions = () => ({ 25 | attachTo: window.domNode 26 | }); 27 | 28 | describe("General", () => { 29 | it("should render without problems with defaults properties", () => { 30 | const wrapper = mount(, { attachTo: window.domNode }); 31 | expect(wrapper).to.exist; 32 | }); 33 | 34 | it("should trigger console warn if editorOption is called", () => { 35 | const stub = sinon.stub(console, "warn"); 36 | const wrapper = mount( 37 | , 38 | getMountOptions() 39 | ); 40 | expect(wrapper).to.exist; 41 | expect( 42 | console.warn.calledWith( 43 | "ReactAce: editor option enableBasicAutocompletion was activated but not found. Did you need to import a related tool or did you possibly mispell the option?" 44 | ) 45 | ).to.be.true; 46 | stub.restore(); 47 | }); 48 | 49 | it("should render without problems with defaults properties, defaultValue and keyboardHandler", () => { 50 | const wrapper = mount( 51 | , 52 | getMountOptions() 53 | ); 54 | expect(wrapper).to.exist; 55 | let editor = wrapper.instance().editor; 56 | expect(editor.getValue()).to.equal("hi james"); 57 | }); 58 | 59 | it("should render editor options not default values", () => { 60 | const wrapper = mount( 61 | , 67 | getMountOptions() 68 | ); 69 | expect(wrapper).to.exist; 70 | let editor = wrapper.instance().editor; 71 | expect(editor.getOption("tabSize")).to.equal(2); 72 | }); 73 | 74 | it("should get the ace library from the onBeforeLoad callback", () => { 75 | const beforeLoadCallback = sinon.spy(); 76 | mount(, getMountOptions()); 77 | 78 | expect(beforeLoadCallback.callCount).to.equal(1); 79 | expect(beforeLoadCallback.getCall(0).args[0]).to.deep.equal(ace); 80 | }); 81 | 82 | it("should get the editor from the onLoad callback", () => { 83 | const loadCallback = sinon.spy(); 84 | const wrapper = mount( 85 | , 86 | getMountOptions() 87 | ); 88 | 89 | // Get the editor 90 | const editor = wrapper.instance().editor; 91 | 92 | expect(loadCallback.callCount).to.equal(1); 93 | expect(loadCallback.getCall(0).args[0]).to.deep.equal(editor); 94 | }); 95 | 96 | it("should set the editor props to the Ace element", () => { 97 | const editorProperties = { 98 | react: "setFromReact", 99 | test: "setFromTest" 100 | }; 101 | const wrapper = mount( 102 | , 103 | getMountOptions() 104 | ); 105 | 106 | const editor = wrapper.instance().editor; 107 | 108 | expect(editor.react).to.equal(editorProperties.react); 109 | expect(editor.test).to.equal(editorProperties.test); 110 | }); 111 | 112 | it("should set the command for the Ace element", () => { 113 | const commandsMock = [ 114 | { 115 | name: "myReactAceTest", 116 | bindKey: { win: "Ctrl-M", mac: "Command-M" }, 117 | exec: () => {}, 118 | readOnly: true 119 | }, 120 | { 121 | name: "myTestCommand", 122 | bindKey: { win: "Ctrl-W", mac: "Command-W" }, 123 | exec: () => {}, 124 | readOnly: true 125 | } 126 | ]; 127 | const wrapper = mount( 128 | , 129 | getMountOptions() 130 | ); 131 | 132 | const editor = wrapper.instance().editor; 133 | expect(editor.commands.commands.myReactAceTest).to.deep.equal( 134 | commandsMock[0] 135 | ); 136 | expect(editor.commands.commands.myTestCommand).to.deep.equal( 137 | commandsMock[1] 138 | ); 139 | }); 140 | 141 | it("should change the command binding for the Ace element", () => { 142 | const commandsMock = [ 143 | { 144 | bindKey: { win: "ctrl-d", mac: "command-d" }, 145 | name: "selectMoreAfter", 146 | exec: "selectMoreAfter" 147 | } 148 | ]; 149 | const wrapper = mount( 150 | , 151 | getMountOptions() 152 | ); 153 | 154 | const editor = wrapper.instance().editor; 155 | const expected = [editor.commands.commands.removeline, "selectMoreAfter"]; 156 | expect(editor.commands.commandKeyBinding["ctrl-d"]).to.deep.equal( 157 | expected 158 | ); 159 | }); 160 | 161 | it("should trigger the focus on mount", () => { 162 | const onFocusCallback = sinon.spy(); 163 | mount( 164 | , 165 | getMountOptions() 166 | ); 167 | 168 | // Read the focus 169 | expect(onFocusCallback.callCount).to.equal(1); 170 | }); 171 | 172 | it("should set up the markers", () => { 173 | const markers = [ 174 | { 175 | startRow: 3, 176 | type: "text", 177 | className: "test-marker" 178 | } 179 | ]; 180 | const wrapper = mount(, getMountOptions()); 181 | 182 | // Read the markers 183 | const editor = wrapper.instance().editor; 184 | expect(editor.getSession().getMarkers()["3"].clazz).to.equal( 185 | "test-marker" 186 | ); 187 | expect(editor.getSession().getMarkers()["3"].type).to.equal("text"); 188 | }); 189 | 190 | it("should update the markers", () => { 191 | const oldMarkers = [ 192 | { 193 | startRow: 4, 194 | type: "text", 195 | className: "test-marker-old" 196 | }, 197 | { 198 | startRow: 7, 199 | type: "foo", 200 | className: "test-marker-old", 201 | inFront: true 202 | } 203 | ]; 204 | const markers = [ 205 | { 206 | startRow: 3, 207 | type: "text", 208 | className: "test-marker-new", 209 | inFront: true 210 | }, 211 | { 212 | startRow: 5, 213 | type: "text", 214 | className: "test-marker-new" 215 | } 216 | ]; 217 | const wrapper = mount( 218 | , 219 | getMountOptions() 220 | ); 221 | 222 | // Read the markers 223 | const editor = wrapper.instance().editor; 224 | expect(editor.getSession().getMarkers()["3"].clazz).to.equal( 225 | "test-marker-old" 226 | ); 227 | expect(editor.getSession().getMarkers()["3"].type).to.equal("text"); 228 | wrapper.setProps({ markers }); 229 | const editorB = wrapper.instance().editor; 230 | 231 | expect(editorB.getSession().getMarkers()["6"].clazz).to.equal( 232 | "test-marker-new" 233 | ); 234 | expect(editorB.getSession().getMarkers()["6"].type).to.equal("text"); 235 | }); 236 | 237 | it("should clear the markers", () => { 238 | const oldMarkers = [ 239 | { 240 | startRow: 4, 241 | type: "text", 242 | className: "test-marker-old" 243 | }, 244 | { 245 | startRow: 7, 246 | type: "foo", 247 | className: "test-marker-old", 248 | inFront: true 249 | } 250 | ]; 251 | const markers = []; 252 | const wrapper = mount( 253 | , 254 | getMountOptions() 255 | ); 256 | 257 | // Read the markers 258 | const editor = wrapper.instance().editor; 259 | expect(editor.getSession().getMarkers()["3"].clazz).to.equal( 260 | "test-marker-old" 261 | ); 262 | expect(editor.getSession().getMarkers()["3"].type).to.equal("text"); 263 | wrapper.setProps({ markers }); 264 | const editorB = wrapper.instance().editor; 265 | 266 | expect(editorB.getSession().getMarkers()).to.deep.equal({}); 267 | }); 268 | 269 | it("should add annotations and clear them", () => { 270 | const annotations = [ 271 | { 272 | row: 3, // must be 0 based 273 | column: 4, // must be 0 based 274 | text: "error.message", // text to show in tooltip 275 | type: "error" 276 | } 277 | ]; 278 | const wrapper = mount(, getMountOptions()); 279 | const editor = wrapper.instance().editor; 280 | wrapper.setProps({ annotations }); 281 | expect(editor.getSession().getAnnotations()).to.deep.equal(annotations); 282 | wrapper.setProps({ annotations: null }); 283 | expect(editor.getSession().getAnnotations()).to.deep.equal([]); 284 | }); 285 | 286 | it("should add annotations with changing editor value", () => { 287 | // See https://github.com/securingsincity/react-ace/issues/300 288 | const annotations = [ 289 | { row: 0, column: 0, text: "error.message", type: "error" } 290 | ]; 291 | const initialText = `Initial 292 | text`; 293 | const modifiedText = `Modified 294 | text`; 295 | const wrapper = mount( 296 | , 297 | getMountOptions() 298 | ); 299 | const editor = wrapper.instance().editor; 300 | wrapper.setProps({ 301 | annotations: annotations, 302 | value: modifiedText 303 | }); 304 | expect(editor.renderer.$gutterLayer.$annotations).to.have.length(1); 305 | expect(editor.renderer.$gutterLayer.$annotations[0]).to.have.property( 306 | "className" 307 | ); 308 | }); 309 | 310 | it("should set editor to null on componentWillUnmount", () => { 311 | const wrapper = mount(, getMountOptions()); 312 | expect(wrapper.getElement().editor).to.not.equal(null); 313 | 314 | // Check the editor is null after the Unmount 315 | wrapper.unmount(); 316 | expect(wrapper.get(0)).to.not.exist; 317 | }); 318 | }); 319 | 320 | //inspired from https://github.com/goodtimeaj/debounce-function/blob/master/test/unit/debounce-function.js 321 | describe("Debounce function", () => { 322 | it("function arg should be called when after timeout", done => { 323 | const wrapper = mount(, getMountOptions()); 324 | var flag = false; 325 | var func = wrapper.instance().debounce(function() { 326 | flag = true; 327 | }, 100); 328 | func(); 329 | expect(flag).to.be.false; 330 | setTimeout(function() { 331 | expect(flag).to.be.true; 332 | done(); 333 | }, 150); 334 | }); 335 | 336 | it("timer should be reset on successive call", done => { 337 | const wrapper = mount(, getMountOptions()); 338 | 339 | var flag = false; 340 | var func = wrapper.instance().debounce(function() { 341 | flag = true; 342 | }, 100); 343 | func(); 344 | expect(flag).to.be.false; 345 | setTimeout(function() { 346 | expect(flag).to.be.false; 347 | func(); 348 | }, 50); 349 | setTimeout(function() { 350 | expect(flag).to.be.false; 351 | }, 120); 352 | setTimeout(function() { 353 | expect(flag).to.be.true; 354 | done(); 355 | }, 160); 356 | }); 357 | 358 | it("function should be called only once per period", done => { 359 | const wrapper = mount(, getMountOptions()); 360 | 361 | var flag1 = false; 362 | var flag2 = false; 363 | var func = wrapper.instance().debounce(function() { 364 | if (flag1) { 365 | flag2 = true; 366 | } 367 | flag1 = true; 368 | }, 100); 369 | 370 | func(); 371 | expect(flag1).to.be.false; 372 | expect(flag2).to.be.false; 373 | setTimeout(function() { 374 | expect(flag1).to.be.false; 375 | expect(flag2).to.be.false; 376 | func(); 377 | setTimeout(function() { 378 | expect(flag1).to.be.true; 379 | expect(flag2).to.be.false; 380 | func(); 381 | setTimeout(function() { 382 | expect(flag1).to.be.true; 383 | expect(flag2).to.be.false; 384 | done(); 385 | }, 90); 386 | }, 110); 387 | }, 50); 388 | }); 389 | it("should keep initial value after undo event", done => { 390 | const onInput = () => { 391 | const editor = wrapper.instance().editor; 392 | editor.undo(); 393 | expect(editor.getValue()).to.equal("foobar"); 394 | done(); 395 | }; 396 | 397 | const wrapper = mount( 398 | , 399 | getMountOptions() 400 | ); 401 | }); 402 | }); 403 | 404 | describe("Events", () => { 405 | it("should call the onChange method callback", () => { 406 | const onChangeCallback = sinon.spy(); 407 | const wrapper = mount( 408 | , 409 | getMountOptions() 410 | ); 411 | 412 | // Check is not previously called 413 | expect(onChangeCallback.callCount).to.equal(0); 414 | 415 | // Trigger the change event 416 | const expectText = "React Ace Test"; 417 | wrapper.instance().editor.setValue(expectText, 1); 418 | 419 | expect(onChangeCallback.callCount).to.equal(1); 420 | expect(onChangeCallback.getCall(0).args[0]).to.equal(expectText); 421 | expect(onChangeCallback.getCall(0).args[1].action).to.eq("insert"); 422 | }); 423 | 424 | it("should limit call to onChange (debounce)", done => { 425 | const period = 100; 426 | const onChangeCallback = sinon.spy(); 427 | const wrapper = mount( 428 | , 429 | getMountOptions() 430 | ); 431 | 432 | // Check is not previously called 433 | expect(onChangeCallback.callCount).to.equal(0); 434 | 435 | // Trigger the change event 436 | const expectText = "React Ace Test"; 437 | const expectText2 = "React Ace Test2"; 438 | wrapper.instance().editor.setValue(expectText, 1); 439 | wrapper.instance().editor.setValue(expectText2, 1); 440 | 441 | expect(onChangeCallback.callCount).to.equal(0); 442 | 443 | setTimeout(function() { 444 | expect(onChangeCallback.callCount).to.equal(1); 445 | expect(onChangeCallback.getCall(0).args[0]).to.equal(expectText2); 446 | expect(onChangeCallback.getCall(0).args[1].action).to.eq("insert"); 447 | onChangeCallback.resetHistory(); 448 | wrapper.instance().editor.setValue(expectText2, 1); 449 | wrapper.instance().editor.setValue(expectText, 1); 450 | expect(onChangeCallback.callCount).to.equal(0); 451 | setTimeout(function() { 452 | expect(onChangeCallback.callCount).to.equal(1); 453 | expect(onChangeCallback.getCall(0).args[0]).to.equal(expectText); 454 | expect(onChangeCallback.getCall(0).args[1].action).to.eq("insert"); 455 | done(); 456 | }, 100); 457 | }, 100); 458 | }); 459 | 460 | it("should call the onCopy method", () => { 461 | const onCopyCallback = sinon.spy(); 462 | const wrapper = mount( 463 | , 464 | getMountOptions() 465 | ); 466 | 467 | // Check is not previously called 468 | expect(onCopyCallback.callCount).to.equal(0); 469 | 470 | // Trigger the copy event 471 | const expectText = "React Ace Test"; 472 | wrapper.instance().onCopy(expectText); 473 | 474 | expect(onCopyCallback.callCount).to.equal(1); 475 | expect(onCopyCallback.getCall(0).args[0]).to.equal(expectText); 476 | }); 477 | 478 | it("should call the onPaste method", () => { 479 | const onPasteCallback = sinon.spy(); 480 | const wrapper = mount( 481 | , 482 | getMountOptions() 483 | ); 484 | 485 | // Check is not previously called 486 | expect(onPasteCallback.callCount).to.equal(0); 487 | 488 | // Trigger the Paste event 489 | const expectText = "React Ace Test"; 490 | wrapper.instance().onPaste(expectText); 491 | 492 | expect(onPasteCallback.callCount).to.equal(1); 493 | expect(onPasteCallback.getCall(0).args[0]).to.equal(expectText); 494 | }); 495 | 496 | it("should call the onFocus method callback", () => { 497 | const onFocusCallback = sinon.spy(); 498 | const wrapper = mount( 499 | , 500 | getMountOptions() 501 | ); 502 | 503 | // Check is not previously called 504 | expect(onFocusCallback.callCount).to.equal(0); 505 | 506 | // Trigger the focus event 507 | wrapper.instance().editor.focus(); 508 | 509 | expect(onFocusCallback.callCount).to.equal(1); 510 | expect(onFocusCallback.args.length).to.equal(1); 511 | }); 512 | 513 | it("should call the onSelectionChange method callback", done => { 514 | let onSelectionChange = function() {}; 515 | const value = ` 516 | function main(value) { 517 | console.log('hi james') 518 | return value; 519 | } 520 | `; 521 | const wrapper = mount(, getMountOptions()); 522 | 523 | onSelectionChange = function(selection) { 524 | const content = wrapper 525 | .instance() 526 | .editor.session.getTextRange(selection.getRange()); 527 | expect(content).to.equal(value); 528 | done(); 529 | }; 530 | wrapper.setProps({ onSelectionChange }); 531 | wrapper 532 | .instance() 533 | .editor.getSession() 534 | .selection.selectAll(); 535 | }); 536 | 537 | it("should call the onCursorChange method callback", done => { 538 | let onCursorChange = function() {}; 539 | const value = ` 540 | function main(value) { 541 | console.log('hi james') 542 | return value; 543 | } 544 | `; 545 | 546 | const wrapper = mount(, getMountOptions()); 547 | onCursorChange = function(selection) { 548 | expect(selection.getCursor()).to.deep.equal({ row: 0, column: 0 }); 549 | done(); 550 | }; 551 | wrapper.setProps({ onCursorChange }); 552 | expect( 553 | wrapper 554 | .instance() 555 | .editor.getSession() 556 | .selection.getCursor() 557 | ).to.deep.equal({ row: 5, column: 6 }); 558 | wrapper 559 | .instance() 560 | .editor.getSession() 561 | .selection.moveCursorTo(0, 0); 562 | }); 563 | 564 | it("should call the onBlur method callback", () => { 565 | const onBlurCallback = sinon.spy(); 566 | const wrapper = mount( 567 | , 568 | getMountOptions() 569 | ); 570 | 571 | // Check is not previously called 572 | expect(onBlurCallback.callCount).to.equal(0); 573 | 574 | // Trigger the blur event 575 | wrapper.instance().onBlur(); 576 | 577 | expect(onBlurCallback.callCount).to.equal(1); 578 | expect(onBlurCallback.args.length).to.equal(1); 579 | }); 580 | 581 | it("should not trigger a component error to call the events without setting the props", () => { 582 | const wrapper = mount(, getMountOptions()); 583 | 584 | // Check the if statement is checking if the property is set. 585 | wrapper.instance().onChange(); 586 | wrapper.instance().onCopy("copy"); 587 | wrapper.instance().onPaste("paste"); 588 | wrapper.instance().onFocus(); 589 | wrapper.instance().onBlur(); 590 | }); 591 | }); 592 | 593 | describe("ComponentDidUpdate", () => { 594 | it("should update the editorOptions on componentDidUpdate", () => { 595 | const options = { 596 | printMargin: 80 597 | }; 598 | const wrapper = mount( 599 | , 600 | getMountOptions() 601 | ); 602 | 603 | // Read set value 604 | const editor = wrapper.instance().editor; 605 | expect(editor.getOption("printMargin")).to.equal(options.printMargin); 606 | 607 | // Now trigger the componentDidUpdate 608 | const newOptions = { 609 | printMargin: 200, 610 | animatedScroll: true 611 | }; 612 | wrapper.setProps({ setOptions: newOptions }); 613 | expect(editor.getOption("printMargin")).to.equal(newOptions.printMargin); 614 | expect(editor.getOption("animatedScroll")).to.equal( 615 | newOptions.animatedScroll 616 | ); 617 | }); 618 | 619 | it("should update the editorOptions on componentDidUpdate", () => { 620 | const wrapper = mount(, getMountOptions()); 621 | 622 | // Read set value 623 | const editor = wrapper.instance().editor; 624 | expect(editor.getOption("minLines")).to.equal(1); 625 | 626 | wrapper.setProps({ minLines: 2 }); 627 | expect(editor.getOption("minLines")).to.equal(2); 628 | }); 629 | 630 | it("should update the mode on componentDidUpdate", () => { 631 | const wrapper = mount(, getMountOptions()); 632 | 633 | // Read set value 634 | const oldMode = wrapper.first("AceEditor").props(); 635 | 636 | wrapper.setProps({ mode: "elixir" }); 637 | const newMode = wrapper.first("AceEditor").props(); 638 | expect(oldMode).to.not.deep.equal(newMode); 639 | }); 640 | 641 | it("should update many props on componentDidUpdate", () => { 642 | const wrapper = mount( 643 | , 653 | getMountOptions() 654 | ); 655 | 656 | // Read set value 657 | const oldMode = wrapper.first("AceEditor").props(); 658 | 659 | wrapper.setProps({ 660 | theme: "solarized", 661 | keyboardHandler: "emacs", 662 | fontSize: 18, 663 | wrapEnabled: false, 664 | showPrintMargin: false, 665 | showGutter: true, 666 | height: "120px", 667 | width: "220px" 668 | }); 669 | const newMode = wrapper.first("AceEditor").props(); 670 | expect(oldMode).to.not.deep.equal(newMode); 671 | }); 672 | 673 | it("should update the className on componentDidUpdate", () => { 674 | const className = "old-class"; 675 | const wrapper = mount( 676 | , 677 | getMountOptions() 678 | ); 679 | 680 | // Read set value 681 | let editor = wrapper.instance().refEditor; 682 | expect(editor.className).to.equal( 683 | " ace_editor ace_hidpi ace-tm old-class" 684 | ); 685 | 686 | // Now trigger the componentDidUpdate 687 | const newClassName = "new-class"; 688 | wrapper.setProps({ className: newClassName }); 689 | editor = wrapper.instance().refEditor; 690 | expect(editor.className).to.equal( 691 | " new-class ace_editor ace_hidpi ace-tm" 692 | ); 693 | }); 694 | 695 | it("should update the value on componentDidUpdate", () => { 696 | const startValue = "start value"; 697 | const wrapper = mount( 698 | , 699 | getMountOptions() 700 | ); 701 | 702 | // Read set value 703 | let editor = wrapper.instance().editor; 704 | expect(editor.getValue()).to.equal(startValue); 705 | 706 | // Now trigger the componentDidUpdate 707 | const newValue = "updated value"; 708 | wrapper.setProps({ value: newValue }); 709 | editor = wrapper.instance().editor; 710 | expect(editor.getValue()).to.equal(newValue); 711 | }); 712 | 713 | it("should trigger the focus on componentDidUpdate", () => { 714 | const onFocusCallback = sinon.spy(); 715 | const wrapper = mount( 716 | , 717 | getMountOptions() 718 | ); 719 | 720 | // Read the focus 721 | expect(onFocusCallback.callCount).to.equal(0); 722 | 723 | // Now trigger the componentDidUpdate 724 | wrapper.setProps({ focus: true }); 725 | expect(onFocusCallback.callCount).to.equal(1); 726 | }); 727 | }); 728 | }); 729 | -------------------------------------------------------------------------------- /tests/src/split.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import React from "react"; 3 | import sinon from "sinon"; 4 | import ace from "ace-builds"; 5 | import Enzyme, { mount } from "enzyme"; 6 | import SplitEditor from "../../src/split.js"; 7 | import Adapter from "@wojtekmaj/enzyme-adapter-react-17"; 8 | 9 | Enzyme.configure({ adapter: new Adapter() }); 10 | 11 | describe("Split Component", () => { 12 | beforeEach(done => { 13 | const div = document.createElement("div"); 14 | window.domNode = div; 15 | document.body.appendChild(div); 16 | done(); 17 | }); 18 | 19 | afterEach(done => { 20 | document.body.removeChild(window.domNode); 21 | done(); 22 | }); 23 | 24 | const getMountOptions = () => ({ 25 | attachTo: window.domNode 26 | }); 27 | 28 | describe("General", () => { 29 | it("should render without problems with defaults properties", () => { 30 | const wrapper = mount(, getMountOptions()); 31 | expect(wrapper).to.exist; 32 | }); 33 | it("should get the ace library from the onBeforeLoad callback", () => { 34 | const beforeLoadCallback = sinon.spy(); 35 | mount( 36 | , 37 | getMountOptions() 38 | ); 39 | 40 | expect(beforeLoadCallback.callCount).to.equal(1); 41 | expect(beforeLoadCallback.getCall(0).args[0]).to.deep.equal(ace); 42 | }); 43 | 44 | it("should trigger console warn if editorOption is called", () => { 45 | const stub = sinon.stub(console, "warn"); 46 | const wrapper = mount( 47 | , 48 | getMountOptions() 49 | ); 50 | expect(wrapper).to.exist; 51 | expect( 52 | console.warn.calledWith( 53 | "ReaceAce: editor option enableBasicAutocompletion was activated but not found. Did you need to import a related tool or did you possibly mispell the option?" 54 | ) 55 | ).to.be.true; 56 | stub.restore(); 57 | }); 58 | 59 | it("should set the editor props to the Ace element", () => { 60 | const editorProperties = { 61 | react: "setFromReact", 62 | test: "setFromTest" 63 | }; 64 | const wrapper = mount( 65 | , 66 | getMountOptions() 67 | ); 68 | 69 | const editor = wrapper.instance().splitEditor; 70 | 71 | expect(editor.react).to.equal(editorProperties.react); 72 | expect(editor.test).to.equal(editorProperties.test); 73 | }); 74 | 75 | it("should update the orientation on componentDidUpdate", () => { 76 | let orientation = "below"; 77 | const wrapper = mount( 78 | , 79 | getMountOptions() 80 | ); 81 | 82 | // Read set value 83 | let editor = wrapper.instance().split; 84 | expect(editor.getOrientation()).to.equal(editor.BELOW); 85 | 86 | // Now trigger the componentDidUpdate 87 | orientation = "beside"; 88 | wrapper.setProps({ orientation }); 89 | editor = wrapper.instance().split; 90 | expect(editor.getOrientation()).to.equal(editor.BESIDE); 91 | }); 92 | 93 | it("should update the orientation on componentDidUpdate", () => { 94 | const wrapper = mount(, getMountOptions()); 95 | 96 | // Read set value 97 | let editor = wrapper.instance().split; 98 | expect(editor.getSplits()).to.equal(2); 99 | 100 | // Now trigger the componentDidUpdate 101 | wrapper.setProps({ splits: 4 }); 102 | editor = wrapper.instance().split; 103 | expect(editor.getSplits()).to.equal(4); 104 | }); 105 | 106 | it("should set the command for the Ace element", () => { 107 | const commandsMock = [ 108 | { 109 | name: "myReactAceTest", 110 | bindKey: { win: "Ctrl-M", mac: "Command-M" }, 111 | exec: () => {}, 112 | readOnly: true 113 | }, 114 | { 115 | name: "myTestCommand", 116 | bindKey: { win: "Ctrl-W", mac: "Command-W" }, 117 | exec: () => {}, 118 | readOnly: true 119 | } 120 | ]; 121 | const wrapper = mount( 122 | , 123 | getMountOptions() 124 | ); 125 | 126 | const editor = wrapper.instance().splitEditor; 127 | expect(editor.commands.commands.myReactAceTest).to.deep.equal( 128 | commandsMock[0] 129 | ); 130 | expect(editor.commands.commands.myTestCommand).to.deep.equal( 131 | commandsMock[1] 132 | ); 133 | }); 134 | 135 | it("should change the command binding for the Ace element", () => { 136 | const commandsMock = [ 137 | { 138 | bindKey: { win: "ctrl-d", mac: "command-d" }, 139 | name: "selectMoreAfter", 140 | exec: "selectMoreAfter" 141 | } 142 | ]; 143 | const wrapper = mount( 144 | , 145 | getMountOptions() 146 | ); 147 | 148 | const editor = wrapper.instance().splitEditor; 149 | const expected = [editor.commands.commands.removeline, "selectMoreAfter"]; 150 | expect(editor.commands.commandKeyBinding["ctrl-d"]).to.deep.equal( 151 | expected 152 | ); 153 | }); 154 | 155 | it("should get the editor from the onLoad callback", () => { 156 | const loadCallback = sinon.spy(); 157 | const wrapper = mount( 158 | , 159 | getMountOptions() 160 | ); 161 | 162 | // Get the editor 163 | const editor = wrapper.instance().split; 164 | 165 | expect(loadCallback.callCount).to.equal(1); 166 | expect(loadCallback.getCall(0).args[0]).to.deep.equal(editor); 167 | }); 168 | 169 | it("should trigger the focus on mount", () => { 170 | const onFocusCallback = sinon.spy(); 171 | mount( 172 | , 173 | getMountOptions() 174 | ); 175 | 176 | // Read the focus 177 | expect(onFocusCallback.callCount).to.equal(1); 178 | }); 179 | 180 | it("should set editor to null on componentWillUnmount", () => { 181 | const wrapper = mount(, getMountOptions()); 182 | expect(wrapper.getElement().editor).to.not.equal(null); 183 | 184 | // Check the editor is null after the Unmount 185 | wrapper.unmount(); 186 | expect(wrapper.get(0)).to.not.exist; 187 | }); 188 | }); 189 | 190 | describe("Events", () => { 191 | it("should call the onChange method callback", () => { 192 | const onChangeCallback = sinon.spy(); 193 | const wrapper = mount( 194 | , 195 | getMountOptions() 196 | ); 197 | 198 | // Check is not previously called 199 | expect(onChangeCallback.callCount).to.equal(0); 200 | 201 | // Trigger the change event 202 | const expectText = "React Ace Test"; 203 | wrapper.instance().splitEditor.setValue(expectText, 1); 204 | 205 | expect(onChangeCallback.callCount).to.equal(1); 206 | expect(onChangeCallback.getCall(0).args[0]).to.deep.equal([ 207 | expectText, 208 | "" 209 | ]); 210 | expect(onChangeCallback.getCall(0).args[1].action).to.eq("insert"); 211 | }); 212 | 213 | it("should call the onCopy method", () => { 214 | const onCopyCallback = sinon.spy(); 215 | const wrapper = mount( 216 | , 217 | getMountOptions() 218 | ); 219 | 220 | // Check is not previously called 221 | expect(onCopyCallback.callCount).to.equal(0); 222 | 223 | // Trigger the copy event 224 | const expectText = "React Ace Test"; 225 | wrapper.instance().onCopy(expectText); 226 | 227 | expect(onCopyCallback.callCount).to.equal(1); 228 | expect(onCopyCallback.getCall(0).args[0]).to.equal(expectText); 229 | }); 230 | 231 | it("should call the onPaste method", () => { 232 | const onPasteCallback = sinon.spy(); 233 | const wrapper = mount( 234 | , 235 | getMountOptions() 236 | ); 237 | 238 | // Check is not previously called 239 | expect(onPasteCallback.callCount).to.equal(0); 240 | 241 | // Trigger the Paste event 242 | const expectText = "React Ace Test"; 243 | wrapper.instance().onPaste(expectText); 244 | 245 | expect(onPasteCallback.callCount).to.equal(1); 246 | expect(onPasteCallback.getCall(0).args[0]).to.equal(expectText); 247 | }); 248 | 249 | it("should call the onFocus method callback", () => { 250 | const onFocusCallback = sinon.spy(); 251 | const wrapper = mount( 252 | , 253 | getMountOptions() 254 | ); 255 | 256 | // Check is not previously called 257 | expect(onFocusCallback.callCount).to.equal(0); 258 | 259 | // Trigger the focus event 260 | wrapper.instance().split.focus(); 261 | 262 | expect(onFocusCallback.callCount).to.equal(1); 263 | }); 264 | 265 | it("should call the onSelectionChange method callback", () => { 266 | const onSelectionChangeCallback = sinon.spy(); 267 | const wrapper = mount( 268 | , 272 | getMountOptions() 273 | ); 274 | 275 | // Check is not previously called 276 | expect(onSelectionChangeCallback.callCount).to.equal(0); 277 | 278 | // Trigger the focus event 279 | wrapper 280 | .instance() 281 | .splitEditor.getSession() 282 | .selection.selectAll(); 283 | 284 | expect(onSelectionChangeCallback.callCount).to.equal(1); 285 | }); 286 | 287 | it("should call the onCursorChange method callback", () => { 288 | const onCursorChangeCallback = sinon.spy(); 289 | 290 | const wrapper = mount( 291 | , 292 | getMountOptions() 293 | ); 294 | 295 | // The changeCursor event is called when the initial value is set 296 | expect(onCursorChangeCallback.callCount).to.equal(1); 297 | 298 | // Trigger the changeCursor event 299 | wrapper 300 | .instance() 301 | .splitEditor.getSession() 302 | .selection.moveCursorTo(0, 0); 303 | 304 | expect(onCursorChangeCallback.callCount).to.equal(2); 305 | }); 306 | 307 | it("should call the onBlur method callback", () => { 308 | const onBlurCallback = sinon.spy(); 309 | const wrapper = mount( 310 | , 311 | getMountOptions() 312 | ); 313 | 314 | // Check is not previously called 315 | expect(onBlurCallback.callCount).to.equal(0); 316 | 317 | // Trigger the blur event 318 | wrapper.instance().onBlur(); 319 | 320 | expect(onBlurCallback.callCount).to.equal(1); 321 | }); 322 | 323 | it("should not trigger a component error to call the events without setting the props", () => { 324 | const wrapper = mount(, getMountOptions()); 325 | 326 | // Check the if statement is checking if the property is set. 327 | wrapper.instance().onChange(); 328 | wrapper.instance().onCopy("copy"); 329 | wrapper.instance().onPaste("paste"); 330 | wrapper.instance().onFocus(); 331 | wrapper.instance().onBlur(); 332 | }); 333 | }); 334 | describe("ComponentDidUpdate", () => { 335 | it("should update the editorOptions on componentDidUpdate", () => { 336 | const options = { 337 | printMargin: 80 338 | }; 339 | const wrapper = mount( 340 | , 341 | getMountOptions() 342 | ); 343 | 344 | // Read set value 345 | const editor = wrapper.instance().splitEditor; 346 | expect(editor.getOption("printMargin")).to.equal(options.printMargin); 347 | 348 | // Now trigger the componentDidUpdate 349 | const newOptions = { 350 | printMargin: 200, 351 | animatedScroll: true 352 | }; 353 | wrapper.setProps({ setOptions: newOptions }); 354 | expect(editor.getOption("printMargin")).to.equal(newOptions.printMargin); 355 | expect(editor.getOption("animatedScroll")).to.equal( 356 | newOptions.animatedScroll 357 | ); 358 | }); 359 | it("should update the editorOptions on componentDidUpdate", () => { 360 | const wrapper = mount(, getMountOptions()); 361 | 362 | // Read set value 363 | const editor = wrapper.instance().splitEditor; 364 | expect(editor.getOption("minLines")).to.equal(1); 365 | 366 | wrapper.setProps({ minLines: 2 }); 367 | expect(editor.getOption("minLines")).to.equal(2); 368 | }); 369 | 370 | it("should update the mode on componentDidUpdate", () => { 371 | const wrapper = mount( 372 | , 373 | getMountOptions() 374 | ); 375 | 376 | // Read set value 377 | const oldMode = wrapper.first("SplitEditor").props(); 378 | 379 | wrapper.setProps({ mode: "elixir" }); 380 | const newMode = wrapper.first("SplitEditor").props(); 381 | expect(oldMode).to.not.deep.equal(newMode); 382 | }); 383 | 384 | it("should update many props on componentDidUpdate", () => { 385 | const wrapper = mount( 386 | , 396 | getMountOptions() 397 | ); 398 | 399 | // Read set value 400 | const oldMode = wrapper.first("SplitEditor").props(); 401 | 402 | wrapper.setProps({ 403 | theme: "solarized", 404 | keyboardHandler: "emacs", 405 | fontSize: 18, 406 | wrapEnabled: false, 407 | showPrintMargin: false, 408 | showGutter: true, 409 | height: "120px", 410 | width: "220px" 411 | }); 412 | const newMode = wrapper.first("SplitEditor").props(); 413 | expect(oldMode).to.not.deep.equal(newMode); 414 | }); 415 | 416 | it("should update the className on componentDidUpdate", () => { 417 | const className = "old-class"; 418 | const wrapper = mount( 419 | , 420 | getMountOptions() 421 | ); 422 | 423 | // Read set value 424 | let editor = wrapper.instance().refEditor; 425 | expect(editor.className).to.equal( 426 | " ace_editor ace_hidpi ace-tm old-class" 427 | ); 428 | 429 | // Now trigger the componentDidUpdate 430 | const newClassName = "new-class"; 431 | wrapper.setProps({ className: newClassName }); 432 | editor = wrapper.instance().refEditor; 433 | expect(editor.className).to.equal( 434 | " new-class ace_editor ace_hidpi ace-tm" 435 | ); 436 | }); 437 | 438 | it("should update the value on componentDidUpdate", () => { 439 | const startValue = "start value"; 440 | const anotherStartValue = "another start value"; 441 | const wrapper = mount( 442 | , 443 | getMountOptions() 444 | ); 445 | 446 | // Read set value 447 | let editor = wrapper.instance().split.getEditor(0); 448 | let editor2 = wrapper.instance().split.getEditor(1); 449 | expect(editor.getValue()).to.equal(startValue); 450 | expect(editor2.getValue()).to.equal(anotherStartValue); 451 | 452 | // Now trigger the componentDidUpdate 453 | const newValue = "updated value"; 454 | const anotherNewValue = "another updated value"; 455 | wrapper.setProps({ value: [newValue, anotherNewValue] }); 456 | editor = wrapper.instance().splitEditor; 457 | editor2 = wrapper.instance().split.getEditor(1); 458 | expect(editor.getValue()).to.equal(newValue); 459 | expect(editor2.getValue()).to.equal(anotherNewValue); 460 | }); 461 | it("should set up the markers", () => { 462 | const markers = [ 463 | [ 464 | { 465 | startRow: 3, 466 | type: "text", 467 | className: "test-marker" 468 | } 469 | ] 470 | ]; 471 | const wrapper = mount( 472 | , 473 | getMountOptions() 474 | ); 475 | 476 | // Read the markers 477 | const editor = wrapper.instance().splitEditor; 478 | expect(editor.getSession().getMarkers()["3"].clazz).to.equal( 479 | "test-marker" 480 | ); 481 | expect(editor.getSession().getMarkers()["3"].type).to.equal("text"); 482 | }); 483 | 484 | it("should update the markers", () => { 485 | const oldMarkers = [ 486 | [ 487 | { 488 | startRow: 4, 489 | type: "text", 490 | className: "test-marker-old" 491 | }, 492 | { 493 | startRow: 7, 494 | type: "foo", 495 | className: "test-marker-old", 496 | inFront: true 497 | } 498 | ] 499 | ]; 500 | const markers = [ 501 | [ 502 | { 503 | startRow: 3, 504 | type: "text", 505 | className: "test-marker-new", 506 | inFront: true 507 | }, 508 | { 509 | startRow: 5, 510 | type: "text", 511 | className: "test-marker-new" 512 | } 513 | ] 514 | ]; 515 | const wrapper = mount( 516 | , 517 | getMountOptions() 518 | ); 519 | 520 | // Read the markers 521 | const editor = wrapper.instance().splitEditor; 522 | expect(editor.getSession().getMarkers()["3"].clazz).to.equal( 523 | "test-marker-old" 524 | ); 525 | expect(editor.getSession().getMarkers()["3"].type).to.equal("text"); 526 | wrapper.setProps({ markers: markers }); 527 | const editorB = wrapper.instance().splitEditor; 528 | expect(editorB.getSession().getMarkers()["6"].clazz).to.equal( 529 | "test-marker-new" 530 | ); 531 | expect(editorB.getSession().getMarkers()["6"].type).to.equal("text"); 532 | }); 533 | 534 | it("should update the markers", () => { 535 | const oldMarkers = [ 536 | [ 537 | { 538 | startRow: 4, 539 | type: "text", 540 | className: "test-marker-old" 541 | }, 542 | { 543 | startRow: 7, 544 | type: "foo", 545 | className: "test-marker-old", 546 | inFront: true 547 | } 548 | ] 549 | ]; 550 | const markers = [[]]; 551 | const wrapper = mount( 552 | , 553 | getMountOptions() 554 | ); 555 | 556 | // Read the markers 557 | const editor = wrapper.instance().splitEditor; 558 | expect(editor.getSession().getMarkers()["3"].clazz).to.equal( 559 | "test-marker-old" 560 | ); 561 | expect(editor.getSession().getMarkers()["3"].type).to.equal("text"); 562 | wrapper.setProps({ markers: markers }); 563 | const editorB = wrapper.instance().splitEditor; 564 | expect(editorB.getSession().getMarkers()).to.deep.equal({}); 565 | }); 566 | 567 | it("should add annotations", () => { 568 | const annotations = [ 569 | { 570 | row: 3, // must be 0 based 571 | column: 4, // must be 0 based 572 | text: "error.message", // text to show in tooltip 573 | type: "error" 574 | } 575 | ]; 576 | const wrapper = mount(, getMountOptions()); 577 | const editor = wrapper.instance().splitEditor; 578 | wrapper.setProps({ annotations: [annotations] }); 579 | expect(editor.getSession().getAnnotations()).to.deep.equal(annotations); 580 | wrapper.setProps({ annotations: null }); 581 | expect(editor.getSession().getAnnotations()).to.deep.equal([]); 582 | }); 583 | 584 | it("should trigger the focus on componentDidUpdate", () => { 585 | const onFocusCallback = sinon.spy(); 586 | const wrapper = mount( 587 | , 588 | getMountOptions() 589 | ); 590 | 591 | // Read the focus 592 | expect(onFocusCallback.callCount).to.equal(0); 593 | 594 | // Now trigger the componentDidUpdate 595 | wrapper.setProps({ focus: true }); 596 | expect(onFocusCallback.callCount).to.equal(1); 597 | }); 598 | }); 599 | }); 600 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for react-ace 4.1.3 2 | // Project: https://github.com/securingsincity/react-ace 3 | // Definitions by: Alberto Nicoletti 4 | 5 | import { Component, CSSProperties } from "react"; 6 | 7 | export interface Annotation { 8 | row: number; 9 | column: number; 10 | type: string; 11 | text: string; 12 | } 13 | 14 | export interface Marker { 15 | startRow: number; 16 | startCol: number; 17 | endRow: number; 18 | endCol: number; 19 | className: string; 20 | type: string; 21 | } 22 | 23 | export interface CommandBindKey { 24 | win: string; 25 | mac: string; 26 | } 27 | 28 | export interface Command { 29 | name: string; 30 | bindKey: CommandBindKey; 31 | exec(editor: any): void; 32 | } 33 | 34 | /** 35 | * See https://github.com/ajaxorg/ace/wiki/Configuring-Ace 36 | */ 37 | export interface AceOptions { 38 | selectionStyle?: "line" | "text"; 39 | highlightActiveLine?: boolean; 40 | highlightSelectedWord?: boolean; 41 | readOnly?: boolean; 42 | cursorStyle?: "ace" | "slim" | "smooth" | "wide"; 43 | mergeUndoDeltas?: false | true | "always"; 44 | behavioursEnabled?: boolean; 45 | wrapBehavioursEnabled?: boolean; 46 | /** this is needed if editor is inside scrollable page */ 47 | autoScrollEditorIntoView?: boolean; 48 | hScrollBarAlwaysVisible?: boolean; 49 | vScrollBarAlwaysVisible?: boolean; 50 | highlightGutterLine?: boolean; 51 | animatedScroll?: boolean; 52 | showInvisibles?: boolean; 53 | showPrintMargin?: boolean; 54 | printMarginColumn?: boolean; 55 | printMargin?: boolean; 56 | fadeFoldWidgets?: boolean; 57 | showFoldWidgets?: boolean; 58 | showLineNumbers?: boolean; 59 | showGutter?: boolean; 60 | displayIndentGuides?: boolean; 61 | /** number or css font-size string */ 62 | fontSize?: number | string; 63 | /** css */ 64 | fontFamily?: string; 65 | maxLines?: number; 66 | minLines?: number; 67 | scrollPastEnd?: boolean; 68 | fixedWidthGutter?: boolean; 69 | /** path to a theme e.g "ace/theme/textmate" */ 70 | theme?: string; 71 | scrollSpeed?: number; 72 | dragDelay?: number; 73 | dragEnabled?: boolean; 74 | focusTimout?: number; 75 | tooltipFollowsMouse?: boolean; 76 | firstLineNumber?: number; 77 | overwrite?: boolean; 78 | newLineMode?: boolean; 79 | useWorker?: boolean; 80 | useSoftTabs?: boolean; 81 | tabSize?: number; 82 | wrap?: boolean; 83 | foldStyle?: boolean; 84 | /** path to a mode e.g "ace/mode/text" */ 85 | mode?: string; 86 | /** on by default */ 87 | enableMultiselect?: boolean; 88 | enableEmmet?: boolean; 89 | enableBasicAutocompletion?: boolean; 90 | enableLiveAutocompletion?: boolean; 91 | enableSnippets?: boolean; 92 | spellcheck?: boolean; 93 | useElasticTabstops?: boolean; 94 | debounceChangePeriod?: number; 95 | } 96 | 97 | export interface EditorProps { 98 | $blockScrolling?: number | boolean; 99 | $blockSelectEnabled?: boolean; 100 | $enableBlockSelect?: boolean; 101 | $enableMultiselect?: boolean; 102 | $highlightPending?: boolean; 103 | $highlightTagPending?: boolean; 104 | $multiselectOnSessionChange?: (...args: any[]) => any; 105 | $onAddRange?: (...args: any[]) => any; 106 | $onChangeAnnotation?: (...args: any[]) => any; 107 | $onChangeBackMarker?: (...args: any[]) => any; 108 | $onChangeBreakpoint?: (...args: any[]) => any; 109 | $onChangeFold?: (...args: any[]) => any; 110 | $onChangeFrontMarker?: (...args: any[]) => any; 111 | $onChangeMode?: (...args: any[]) => any; 112 | $onChangeTabSize?: (...args: any[]) => any; 113 | $onChangeWrapLimit?: (...args: any[]) => any; 114 | $onChangeWrapMode?: (...args: any[]) => any; 115 | $onCursorChange?: (...args: any[]) => any; 116 | $onDocumentChange?: (...args: any[]) => any; 117 | $onMultiSelect?: (...args: any[]) => any; 118 | $onRemoveRange?: (...args: any[]) => any; 119 | $onScrollLeftChange?: (...args: any[]) => any; 120 | $onScrollTopChange?: (...args: any[]) => any; 121 | $onSelectionChange?: (...args: any[]) => any; 122 | $onSingleSelect?: (...args: any[]) => any; 123 | $onTokenizerUpdate?: (...args: any[]) => any; 124 | } 125 | 126 | export interface AceEditorProps { 127 | name?: string; 128 | /** For available modes see https://github.com/ajaxorg/ace/tree/master/lib/ace/mode */ 129 | mode?: string; 130 | /** For available themes see https://github.com/ajaxorg/ace/tree/master/lib/ace/theme */ 131 | theme?: string; 132 | height?: string; 133 | width?: string; 134 | className?: string; 135 | fontSize?: number; 136 | showGutter?: boolean; 137 | showPrintMargin?: boolean; 138 | highlightActiveLine?: boolean; 139 | focus?: boolean; 140 | cursorStart?: number; 141 | wrapEnabled?: boolean; 142 | readOnly?: boolean; 143 | minLines?: number; 144 | maxLines?: number; 145 | enableBasicAutocompletion?: boolean; 146 | enableLiveAutocompletion?: boolean; 147 | tabSize?: number; 148 | value?: string; 149 | defaultValue?: string; 150 | scrollMargin?: number[]; 151 | onLoad?: (editor: EditorProps) => void; 152 | onBeforeLoad?: (ace: any) => void; 153 | onChange?: (value: string, event?: any) => void; 154 | onInput?: (value: string, event?: any) => void; 155 | onSelection?: (selectedText: string, event?: any) => void; 156 | onCopy?: (value: string) => void; 157 | onPaste?: (value: string) => void; 158 | onFocus?: (event: any) => void; 159 | onBlur?: (event: any) => void; 160 | onValidate?: (annotations: Array) => void; 161 | onScroll?: (editor: EditorProps) => void; 162 | editorProps?: EditorProps; 163 | setOptions?: AceOptions; 164 | keyboardHandler?: string; 165 | commands?: Array; 166 | annotations?: Array; 167 | markers?: Array; 168 | style?: CSSProperties; 169 | } 170 | 171 | export default class AceEditor extends Component {} 172 | -------------------------------------------------------------------------------- /webpack.config.example.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | 4 | 5 | module.exports = { 6 | mode: 'development', 7 | devtool: 'source-map', 8 | entry: { 9 | 'index': './example/index', 10 | 'split': './example/split', 11 | 'diff': './example/diff', 12 | }, 13 | output: { 14 | path: path.join(__dirname, 'example/static'), 15 | filename: '[name].js', 16 | publicPath: 'static/', 17 | }, 18 | optimization: { chunkIds: 'total-size', moduleIds: 'size' }, 19 | plugins: [ 20 | new webpack.HotModuleReplacementPlugin(), 21 | ], 22 | module: { 23 | rules: [{ 24 | test: /(\.js|\.jsx)$/, 25 | use: { 26 | loader: 'babel-loader' 27 | }, 28 | exclude: /node_modules/, 29 | }], 30 | }, 31 | devServer: { 32 | hot: true, 33 | contentBase: [path.join(__dirname, 'example'), path.join(__dirname, 'dist')], 34 | compress: true, 35 | port: 9000, 36 | } 37 | }; 38 | --------------------------------------------------------------------------------