├── .babelrc ├── .eslintrc ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ ├── node.js.yml │ └── npm-publish.yml ├── .gitignore ├── .mergify.yml ├── .npmignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── docs ├── Ace.md ├── Diff.md ├── FAQ.md ├── Migrate-v7-to-v8.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.tsx ├── diff.tsx ├── editorOptions.ts ├── index.ts ├── split.tsx └── types.ts ├── tests ├── setup.js └── src │ ├── ace.spec.js │ └── split.spec.js ├── tsconfig.json ├── tslint.json ├── types.d.ts ├── webpack.config.base.js ├── webpack.config.development.js ├── webpack.config.example.js └── webpack.config.production.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | // "transform-object-rest-spread" 8 | ] 9 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 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 | "globals": { 16 | "ENVIRONMENT": true, 17 | "STANDALONE": true, 18 | "it": true, 19 | "describe": true, 20 | "xdescribe": true, 21 | "xit": true, 22 | "before": true, 23 | "beforeEach": true, 24 | "after": true, 25 | "afterEach": true 26 | }, 27 | "rules": { 28 | "linebreak-style": [ 29 | "error", 30 | "unix" 31 | ], 32 | "no-console": [ 33 | "error", 34 | { 35 | "allow": [ 36 | "warn", 37 | "error" 38 | ] 39 | } 40 | ], 41 | "react/no-deprecated": "warn" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | github: securingsincity 3 | open_collective: react-ace 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | open-pull-requests-limit: 25 13 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [main] 9 | schedule: 10 | - cron: '0 17 * * 3' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | # Override automatic language detection by changing the below list 21 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 22 | language: ['javascript'] 23 | # Learn more... 24 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v2 29 | with: 30 | # We must fetch at least the immediate parents so that if this is 31 | # a pull request then we can checkout the head. 32 | fetch-depth: 2 33 | 34 | # If this run was triggered by a pull request event, then checkout 35 | # the head of the pull request instead of the merge commit. 36 | - run: git checkout HEAD^2 37 | if: ${{ github.event_name == 'pull_request' }} 38 | 39 | # Initializes the CodeQL tools for scanning. 40 | - name: Initialize CodeQL 41 | uses: github/codeql-action/init@v1 42 | with: 43 | languages: ${{ matrix.language }} 44 | 45 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 46 | # If this step fails, then you should remove it and run the build manually (see below) 47 | - name: Autobuild 48 | uses: github/codeql-action/autobuild@v1 49 | 50 | # ℹ️ Command-line programs to run using the OS shell. 51 | # 📚 https://git.io/JvXDl 52 | 53 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 54 | # and modify them (or add more) to build your code if your project 55 | # uses a compiled language 56 | 57 | #- run: | 58 | # make bootstrap 59 | # make release 60 | 61 | - name: Perform CodeQL Analysis 62 | uses: github/codeql-action/analyze@v1 63 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: CI 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | pull_request: 10 | branches: [main] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [16.x] 19 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm ci 28 | - run: npm run build --if-present 29 | - run: npm run lint 30 | - run: npm run coverage 31 | - run: npm run build:example 32 | # - name: Coveralls 33 | # uses: coverallsapp/github-action@master 34 | # with: 35 | # github-token: ${{ secrets.GITHUB_TOKEN }} 36 | 37 | - name: Deploy 38 | if: ${{ github.ref == 'refs/heads/main' }} 39 | uses: peaceiris/actions-gh-pages@v3 40 | with: 41 | github_token: ${{ secrets.GITHUB_TOKEN }} 42 | publish_dir: ./example 43 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Node.js Package 2 | on: 3 | release: 4 | types: [published] 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | with: 12 | # "ref" specifies the branch to check out. 13 | # "github.event.release.target_commitish" is a global variable and specifies the branch the release targeted 14 | ref: ${{ github.event.release.target_commitish }} 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: 16 18 | registry-url: https://registry.npmjs.org/ 19 | - run: npm ci 20 | - run: npm test 21 | - run: npm run build 22 | - run: git config --global user.name "GitHub CD bot" 23 | - run: git config --global user.email "james.hrisho@gmail.com" 24 | - run: npm version ${{ github.event.release.tag_name }} --no-git-tag-version 25 | - run: git add . && git commit -m "[Release] ${{ github.event.release.tag_name }}" 26 | - run: npm publish 27 | env: 28 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 29 | - name: Upload a Build Artifact 30 | uses: actions/upload-artifact@v2.2.2 31 | with: 32 | # Artifact name 33 | name: 34 | react-ace.min.js 35 | # A file, directory or wildcard pattern that describes what to upload 36 | path: 37 | dist/react-ace.min.js 38 | # The desired behavior if no files are found using the provided path. 39 | - run: git push 40 | env: 41 | github-token: ${{ secrets.GITHUB_TOKEN }} 42 | -------------------------------------------------------------------------------- /.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 | dist 45 | example/static -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: Automatic merge on approval 3 | conditions: 4 | - "#approved-reviews-by>=1" 5 | - check-success=CodeQL 6 | - check-success=build (16.x) 7 | - base=main 8 | actions: 9 | merge: 10 | method: merge 11 | - name: Automatic merge of dependabot 12 | conditions: 13 | - author~=^dependabot(|-preview)\[bot\]$ 14 | - check-success=CodeQL 15 | - check-success=build (16.x) 16 | - base=main 17 | actions: 18 | merge: 19 | method: merge -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | coverage 4 | example 5 | tests 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | ## 10.0.0 3 | - Fix changing value loses annotations #1429 4 | - Fix Uncaught TypeError: (reading 'destroy') #1293 5 | - Upgrade dev dependencies 6 | - Upgrade Ace-builds to 1.4.14 7 | 8 | 9 | ## 9.5.0 10 | - Upgrade dev dependencies 11 | - Upgrade Ace-builds to 1.4.13 12 | 13 | ## 9.4.4 14 | 15 | - Upgrade dev dependencies 16 | 17 | ## 9.4.2 18 | 19 | - Upgrade dev dependencies 20 | 21 | ## 9.4.1 22 | 23 | - Upgrade dev dependencies 24 | 25 | ## 9.4.0 26 | 27 | - Upgrade dev dependencies 28 | - Prevent no theme error #1038 29 | 30 | ## 9.3.0 31 | 32 | - Upgrade dev dependencies 33 | - Update annotations type #998 34 | - Accept an object as mode #1001 35 | 36 | ## 9.2.1 37 | 38 | - Bump ace-builds from 1.4.11 to 1.4.12 39 | - Import Range from ace-builds in ace.tsx to match split.tsx 40 | - Fix type for exec param in ICommand interface 41 | - Add React 17 Support 42 | - Upgrade dev dependencies 43 | 44 | ## 9.1.4 45 | 46 | - Use https for demo site 47 | - style prop type to conditional object 48 | - Security fixes and dev dependencies upgrades 49 | 50 | ## 9.1.3 51 | 52 | - Upgrade dev dependencies 53 | - Allow Editor to be reset to empty value by passing an empty value. (#895) 54 | 55 | ## 9.1.2 56 | 57 | - Upgrade dependencies including lodash 58 | 59 | ## 9.1.1 60 | 61 | - Upgrade ace-builds to 1.4.11 62 | - Upgrade dev dependencies 63 | 64 | ## 9.1.0 65 | 66 | - fix typescript binding for ICommand.exec() (#860) 67 | - Remove default value for editor value. (#857) 68 | - fix no ace_editor.css in shadow dom (#864) 69 | - update build dependencies, build scripts, and prettier 70 | 71 | ## 9.0.0 72 | 73 | - Use Ace.Editor types - Possible breaking change (#828) 74 | - Upgrade dev dependencies 75 | - Enables simple Server Side Rendering support (#841) 76 | 77 | ## 8.1.0 78 | 79 | - Fixes iAceEditorProps.mode prop to include support custom mode 80 | - Fix theme not set properly for split editor (#785) 81 | - Fixes types for print margin option 82 | - Upgrade dev dependencies 83 | 84 | ## 8.0.0 85 | 86 | _BREAKING CHANGES!!!_ 87 | 88 | - Removes brace as a dependency for ace-builds 89 | - Updates all documentation 90 | - Updates all examples 91 | 92 | ## 7.0.5 93 | 94 | - Upgrades dev dependencies 95 | 96 | ## 7.0.4 (7.0.3 was busted) 97 | 98 | - Upgrades dev dependencies 99 | - Export types 100 | - Remove babel polyfill 101 | 102 | ## 7.0.2 103 | 104 | - Support node 11 in development 105 | - Upgrade dev dependencies 106 | - Add typings for EditorOptions/EditorEvents, remove index signatures #651 107 | - Fix types #652 108 | 109 | ## 7.0.1 110 | 111 | - Fix types #646 112 | 113 | ## 7.0.0 114 | 115 | - Fully move to TypeScript interally 116 | - Publish typings for the split and diff editor 117 | 118 | ## 6.6.0 119 | 120 | - Upgrade dependencies 121 | 122 | ## 6.5.0 123 | 124 | - Upgrade dependencies 125 | - Do not clear active line and active word markers #604 126 | - New 'placeholder' prop to specify placeholder text #603 127 | - Added optional prop to disable navigating to end of file #602 128 | 129 | ## 6.4.0 130 | 131 | - Upgrade types 132 | - Upgrade webpack, sinon 133 | 134 | ## 6.3.2 135 | 136 | - Move `husky` and `pretty-quick` to devDependencies 137 | 138 | ## 6.3.1 139 | 140 | - Fix npm deployments 141 | - Support ace.require to fallback to the CDN version of Ace. 142 | 143 | ## 6.2.2 144 | 145 | - Upgrade dev dependencies (webpack,jsdom,react) 146 | - In type definitions, move debounceChangePeriod from AceOptions 147 | 148 | ## 6.2.1 149 | 150 | - Add editor to onFocus event as per issue #389 151 | - Upgraded webpack 152 | - Add exec argument in ts #535 153 | - Prettier as part of the build 154 | 155 | ## 6.2.0 156 | 157 | - Support for React 17 158 | - Upgraded dependencies 159 | - AceOptions interface adds debounceChangePeriod 160 | - update types 161 | 162 | ## 6.1.4 163 | 164 | - Fixes #479 Diff component does not refresh when value prop changes 165 | 166 | ## 6.1.3 167 | 168 | - Fixes #300 where users were not able to set annotations for multiline text that is changed 169 | 170 | ## 6.1.2 171 | 172 | - Additional Diff documentation 173 | - Add className to diff 174 | - Add Logo to docs 175 | - upgrade dev dependencies 176 | 177 | ## 6.1.1 178 | 179 | - Fixes typo in `console.warn` 180 | - Adds style property to typings 181 | 182 | ## 6.1.0 183 | 184 | - Onchange support in diff editor 185 | - Debounce Prop support in split editor 186 | 187 | ## 6.0.0 188 | 189 | - Adds Diff editor 190 | 191 | ## 5.10.0 192 | 193 | - Upgraded many build dependencies 194 | - Split editor adds UndoManager 195 | 196 | ## 5.9.0 197 | 198 | - First value resets undo manager. Closes #339 and #223 199 | - Updated split editor documentation 200 | 201 | ## 5.8.0 202 | 203 | - Upgrade brace to 0.11 204 | - More loose comparison for componentDidMount for default value. Closes #317. Thanks @VijayKrish93 205 | 206 | ## 5.7.0 207 | 208 | - Adds debounce option for onChange event 209 | - Add support onCursorChange event 210 | - Adds editor as second argument to the onBlur 211 | 212 | ## 5.5.0 213 | 214 | - Adds the onInput event 215 | 216 | ## 5.4.0 217 | 218 | - #285: Added the possibility to change key bindings of existing commands. thanks to @FurcyPin 219 | 220 | ## 5.3.0 221 | 222 | - Adds support for React 16 thanks to @layershifter 223 | - Removes react and react-dom from build. thanks to @M-ZubairAhmed 224 | 225 | ## 5.2.1 and 5.2.2 226 | 227 | - Remove Open Collective from build 228 | 229 | ## 5.2.0 230 | 231 | - Add support for events in onBlur and onFocus callbacks 232 | - Adds onValidate callback 233 | 234 | ## 5.1.2 235 | 236 | - Resize on component did mount and component did update. Fixes #207 and #212. 237 | 238 | ## 5.1.1 239 | 240 | - Fix TypeScript definitions for EditorProps 241 | 242 | ## 5.1.0 243 | 244 | - Editor options do not get reverted due to default props #226 245 | - Markers can be unset to an empty value #229 246 | - Typescript update to set state to empty object instead of undefined 247 | 248 | ## 5.0.1 249 | 250 | - Fixes file extension issue related to `5.0.0`. 251 | 252 | ## 5.0.0 253 | 254 | - 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) 255 | - Ace Editor will now warn on mispelled editor options 256 | - All new documentation 257 | 258 | ## 4.4.0 259 | 260 | - Ace's resize method will be called when the prop `width` changes 261 | 262 | ## 4.3.0 263 | 264 | - Adds support for `onSelectionChange` event 265 | - Add the `Event` as an optional argument to the `onChange` prop 266 | - All new examples 267 | 268 | ## 4.2.2 269 | 270 | - [bugfix] should not handle markers without any markers 271 | 272 | ## 4.2.1 273 | 274 | - Use `prop-type` package instead of React.PropType 275 | 276 | ## 4.2.0 277 | 278 | - Fix `ref` related error 279 | 280 | ## 4.1.6 281 | 282 | - Reverse `PureComponent` use in AceEditor back to `Component` 283 | 284 | ## 4.1.5 285 | 286 | - Add ability to set `scrollMargins` 287 | 288 | ## 4.1.4 289 | 290 | - TypeScript Definitions 291 | -------------------------------------------------------------------------------- /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 2 | 3 | ![logo](https://github.com/securingsincity/react-ace/raw/master/logo.png) 4 | 5 | [![Backers on Open Collective](https://opencollective.com/react-ace/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/react-ace/sponsors/badge.svg)](#sponsors) [![Greenkeeper badge](https://badges.greenkeeper.io/securingsincity/react-ace.svg)](https://greenkeeper.io/) 6 | 7 | [![npm version](https://badge.fury.io/js/react-ace.svg)](http://badge.fury.io/js/react-ace) 8 | [![CDNJS](https://img.shields.io/cdnjs/v/react-ace.svg)](https://cdnjs.com/libraries/react-ace) 9 | [![Coverage Status](https://coveralls.io/repos/github/securingsincity/react-ace/badge.svg?branch=main)](https://coveralls.io/github/securingsincity/react-ace?branch=main) 10 | 11 | 12 | A set of react components for Ace 13 | 14 | _NOTE FOR VERSION 8!_ : We have stopped support for Brace and now use Ace-builds. Please read the documentation on how to migrate. Examples are being updated. 15 | 16 | [DEMO of React Ace](https://securingsincity.github.io/react-ace/) 17 | 18 | [DEMO of React Ace Split Editor](https://securingsincity.github.io/react-ace/split.html) 19 | 20 | [DEMO of React Ace Diff Editor](https://securingsincity.github.io/react-ace/diff.html) 21 | 22 | ## Install 23 | 24 | `npm install react-ace ace-builds` 25 | 26 | `yarn add react-ace ace-builds` 27 | 28 | ## Basic Usage 29 | 30 | ```javascript 31 | import React from "react"; 32 | import { render } from "react-dom"; 33 | import AceEditor from "react-ace"; 34 | 35 | import "ace-builds/src-noconflict/mode-java"; 36 | import "ace-builds/src-noconflict/theme-github"; 37 | import "ace-builds/src-noconflict/ext-language_tools"; 38 | 39 | function onChange(newValue) { 40 | console.log("change", newValue); 41 | } 42 | 43 | // Render editor 44 | render( 45 | , 52 | document.getElementById("example") 53 | ); 54 | ``` 55 | 56 | ## Examples 57 | 58 | Checkout the `example` directory for a working example using webpack. 59 | 60 | ## Documentation 61 | 62 | [Ace Editor](https://github.com/securingsincity/react-ace/blob/master/docs/Ace.md) 63 | 64 | [Split View Editor](https://github.com/securingsincity/react-ace/blob/master/docs/Split.md) 65 | 66 | [Diff Editor](https://github.com/securingsincity/react-ace/blob/master/docs/Diff.md) 67 | 68 | [How to add modes, themes and keyboard handlers](https://github.com/securingsincity/react-ace/blob/master/docs/Modes.md) 69 | 70 | [Frequently Asked Questions](https://github.com/securingsincity/react-ace/blob/master/docs/FAQ.md) 71 | 72 | [Migrate to version 8](https://github.com/securingsincity/react-ace/blob/master/docs/Migrate-v7-to-v8.md) 73 | 74 | ## Backers 75 | 76 | Support us with a monthly donation and help us continue our activities. [[Become a backer](https://opencollective.com/react-ace#backer)] 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | ## Sponsors 110 | 111 | Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/react-ace#sponsor)] 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /docs/Ace.md: -------------------------------------------------------------------------------- 1 | # Ace Editor 2 | 3 | This is the main component of React-Ace. It creates an instance of the Ace Editor. 4 | 5 | ## Demo 6 | 7 | http://securingsincity.github.io/react-ace/ 8 | 9 | ## Example Code 10 | 11 | ```javascript 12 | import React from "react"; 13 | import { render } from "react-dom"; 14 | import AceEditor from "react-ace"; 15 | 16 | import "ace-builds/src-noconflict/mode-java"; 17 | import "ace-builds/src-noconflict/theme-github"; 18 | import "ace-builds/src-noconflict/ext-language_tools" 19 | 20 | function onChange(newValue) { 21 | console.log("change", newValue); 22 | } 23 | 24 | // Render editor 25 | render( 26 | , 38 | document.getElementById("example") 39 | ); 40 | ``` 41 | 42 | ## Available Props 43 | 44 | | Prop | Default | Type | Description | 45 | | ------------------------- | ------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 46 | | name | 'ace-editor' | String | Unique Id to be used for the editor | 47 | | placeholder | null | String | Placeholder text to be displayed when editor is empty | 48 | | mode | '' | String | Language for parsing and code highlighting | 49 | | theme | '' | String | theme to use | 50 | | value | '' | String | value you want to populate in the code highlighter | 51 | | defaultValue | '' | String | Default value of the editor | 52 | | height | '500px' | String | CSS value for height | 53 | | width | '500px' | String | CSS value for width | 54 | | className | | String | custom className | 55 | | fontSize | 12 | Number | pixel value for font-size | 56 | | showGutter | true | Boolean | show gutter | 57 | | showPrintMargin | true | Boolean | show print margin | 58 | | highlightActiveLine | true | Boolean | highlight active line | 59 | | focus | false | Boolean | whether to focus | 60 | | cursorStart | 1 | Number | the location of the cursor | 61 | | wrapEnabled | false | Boolean | Wrapping lines | 62 | | readOnly | false | Boolean | make the editor read only | 63 | | minLines | | Number | Minimum number of lines to be displayed | 64 | | maxLines | | Number | Maximum number of lines to be displayed | 65 | | enableBasicAutocompletion | false | Boolean | Enable basic autocompletion | 66 | | enableLiveAutocompletion | false | Boolean | Enable live autocompletion | 67 | | enableSnippets | false | Boolean | Enable snippets | 68 | | tabSize | 4 | Number | tabSize | 69 | | debounceChangePeriod | null | Number | A debounce delay period for the onChange event | 70 | | onLoad | | Function | called on editor load. The first argument is the instance of the editor | 71 | | onBeforeLoad | | Function | called before editor load. the first argument is an instance of `ace` | 72 | | onChange | | Function | occurs on document change it has 2 arguments the value and the event. | 73 | | onCopy | | Function | triggered by editor `copy` event, and passes text as argument | 74 | | onPaste | | Function | Triggered by editor `paste` event, and passes text as argument | 75 | | 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 | 76 | | 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 | 77 | | onFocus | | Function | triggered by editor `focus` event | 78 | | onBlur | | Function | triggered by editor `blur` event.It has two arguments event and editor | 79 | | onInput | | Function | triggered by editor `input` event | 80 | | onScroll | | Function | triggered by editor `scroll` event | 81 | | onValidate | | Function | triggered, when annotations are changed | 82 | | editorProps | | Object | properties to apply directly to the Ace editor instance | 83 | | setOptions | | Object | [options](https://github.com/ajaxorg/ace/wiki/Configuring-Ace) to apply directly to the Ace editor instance | 84 | | keyboardHandler | | String | corresponding to the keybinding mode to set (such as vim or emacs) | 85 | | commands | | Array | new commands to add to the editor | 86 | | annotations | | Array | annotations to show in the editor i.e. `[{ row: 0, column: 2, type: 'error', text: 'Some error.'}]`, displayed in the gutter.(type:'error','info','warning') | 87 | | 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. | 88 | | style | | Object | camelCased properties | 89 | -------------------------------------------------------------------------------- /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 | ## Example Code 8 | 9 | ```typescript 10 | import React, { Component } from "react"; 11 | import { render } from "react-dom"; 12 | import { diff as DiffEditor } from "react-ace"; 13 | 14 | import "ace-builds/src-noconflict/theme-github"; 15 | 16 | render( 17 | 23 | ); 24 | ``` 25 | 26 | Also see the [diff](../example/diff.js) [example](../example/diff.html) in the example folder for more robust sample code (seen in the [demo](http://securingsincity.github.io/react-ace/diff.html)). 27 | 28 | ## Available Props 29 | 30 | | Prop | Default | Type | Description | 31 | | ------------------------- | ------------ | ---------------- | ----------------------------------------------------------------------------------------------------------- | 32 | | cursorStart | 1 | Number | the location of the cursor | 33 | | editorProps | | Object | properties to apply directly to the Ace editor instance | 34 | | enableBasicAutocompletion | false | Boolean | Enable basic autocompletion | 35 | | enableLiveAutocompletion | false | Boolean | Enable live autocompletion | 36 | | focus | false | Boolean | Whether to focus | 37 | | fontSize | 12 | Number | pixel value for font-size | 38 | | height | '500px' | String | CSS value for height | 39 | | highlightActiveLine | true | Boolean | highlight active line | 40 | | maxLines | | Number | Maximum number of lines to be displayed | 41 | | minLines | | Number | Minimum number of lines to be displayed | 42 | | mode | '' | String | The language to be used for the editor (Java, Javascript, Ruby, etc.) | 43 | | name | 'ace-editor' | string | Unique ID to be used for the split editor | 44 | | onLoad | | Function | called on editor load. The first argument is the instance of the editor | 45 | | onScroll | | Function | triggered by editor `scroll` event | 46 | | onChange | | Function | occurs on document change it has one argument the values array | 47 | | onPaste | | Function | Triggered by editor `paste` event, and passes text as argument | 48 | | orientation | 'beside' | String | The orientation of splits either 'beside' or 'below' | 49 | | readOnly | false | Boolean | make the editor read only | 50 | | scrollMargin | [0, 0, 0, 0] | Array of Numbers | Sets the scroll margins | 51 | | setOptions | | Object | [options](https://github.com/ajaxorg/ace/wiki/Configuring-Ace) to apply directly to the Ace editor instance | 52 | | showGutter | true | Boolean | show gutter | 53 | | showPrintMargin | true | Boolean | show print margin | 54 | | style | | Object | camelCased properties | 55 | | tabSize | 4 | Number | Number of spaces to include as tab | 56 | | theme | 'github' | String | Theme to use | 57 | | value | ['',''] | Array of Strings | Index 0: Value of first editor. Index 1: Value of second editor | 58 | | width | '500px' | String | CSS value for width | 59 | | wrapEnabled | true | Boolean | Whether lines wrap on the editor | 60 | -------------------------------------------------------------------------------- /docs/FAQ.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | ## How do I use it with `preact`? `webpack`? `create-react-app`? 4 | 5 | Check out the example applications 6 | 7 | - [create-react-app](https://github.com/securingsincity/react-ace-create-react-app-example) 8 | - [preact](https://github.com/securingsincity/react-ace-preact-example) 9 | - [webpack](https://github.com/securingsincity/react-ace-webpack-example) 10 | 11 | ## How do I call methods on the editor? How do I call Undo or Redo? 12 | 13 | `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: 14 | 15 | ```javascript 16 | const reactAceComponent = this.refs.reactAceComponent; 17 | const editor = reactAceComponent.editor; 18 | editor.find(searchRegex, { 19 | backwards: false, 20 | wrap: true, 21 | caseSensitive: false, 22 | wholeWord: false, 23 | regExp: true 24 | }); 25 | ``` 26 | 27 | Similarly, if you want to redo or undo, you can reference the editor from the refs 28 | 29 | ```jsx 30 | 31 | 32 | ``` 33 | 34 | ## How do I set editor options like setting block scrolling to infinity? 35 | 36 | ```javascript 37 | 42 | ``` 43 | 44 | ## How do I add language snippets? 45 | 46 | You can import the snippets and mode directly through `ace-builds` along with the language_tools. Here is an example below 47 | 48 | ```javascript 49 | import React from "react"; 50 | import { render } from "react-dom"; 51 | import AceEditor from "react-ace"; 52 | 53 | import "ace-builds/src-min-noconflict/ext-language_tools"; 54 | import "ace-builds/src-noconflict/mode-python"; 55 | import "ace-builds/src-noconflict/snippets/python"; 56 | import "ace-builds/src-noconflict/theme-github"; 57 | 58 | function onChange(newValue) { 59 | console.log("change", newValue); 60 | } 61 | 62 | // Render editor 63 | render( 64 | , 74 | document.getElementById("example") 75 | ); 76 | ``` 77 | 78 | ## How do I get selected text `onSelectionChange`? 79 | 80 | How you extract the text from the editor is based on how to call methods on the editor. 81 | 82 | Your `onSelectionChange` should look like this: 83 | 84 | ```javascript 85 | onSelectionChange(selection) { 86 | const content = this.refs.aceEditor.editor.session.getTextRange(selection.getRange()); 87 | // use content 88 | } 89 | ``` 90 | 91 | ## How do I get selected text ? 92 | 93 | ```javascript 94 | const selectedText = this.refs.aceEditor.editor.getSelectedText(); 95 | // selectedText contains the selected text. 96 | } 97 | ``` 98 | 99 | ## How do I add markers? 100 | 101 | ```javascript 102 | const markers = [ 103 | { 104 | startRow: 3, 105 | type: "text", 106 | className: "test-marker" 107 | } 108 | ]; 109 | const wrapper = ; 110 | ``` 111 | 112 | ## How do I add annotations? 113 | 114 | ```javascript 115 | const annotations = [ 116 | { 117 | row: 3, // must be 0 based 118 | column: 4, // must be 0 based 119 | text: "error.message", // text to show in tooltip 120 | type: "error" 121 | } 122 | ]; 123 | const editor = ; 124 | ``` 125 | 126 | ## How do I add key-bindings? 127 | 128 | ```javascript 129 | render() { 130 | return
131 | { console.log('key-binding used')} //function to execute when keys are pressed. 139 | }]} 140 | /> 141 |
; 142 | } 143 | ``` 144 | 145 | ## How do I change key-bindings for an existing command? 146 | 147 | Same syntax as above, where `exec` is given the name of the command to rebind. 148 | 149 | ```javascript 150 | render() { 151 | return
152 | 162 |
; 163 | } 164 | ``` 165 | 166 | ## How do I add the search box? 167 | 168 | Add the following line 169 | 170 | `import 'ace-builds/src-min-noconflict/ext-searchbox';` 171 | 172 | before introducing the component and it will add the search box. 173 | 174 | ## How do I add a custom mode? 175 | 176 | 1. Create my custom mode class (pure ES6 code) 177 | 2. Initialize the component with an existing mode name (such as "sql") 178 | 3. Use the `componentDidMount` function and call `session.setMode` with an instance of my custom mode. 179 | 180 | My custom mode is: 181 | 182 | ```javascript 183 | import "ace-builds/src-noconflict/mode-java"; 184 | 185 | export class CustomHighlightRules extends window.ace.acequire( 186 | "ace/mode/text_highlight_rules" 187 | ).TextHighlightRules { 188 | constructor() { 189 | super(); 190 | this.$rules = { 191 | start: [ 192 | { 193 | token: "comment", 194 | regex: "#.*$" 195 | }, 196 | { 197 | token: "string", 198 | regex: '".*?"' 199 | } 200 | ] 201 | }; 202 | } 203 | } 204 | 205 | export default class CustomSqlMode extends window.ace.acequire("ace/mode/java") 206 | .Mode { 207 | constructor() { 208 | super(); 209 | this.HighlightRules = CustomHighlightRules; 210 | } 211 | } 212 | ``` 213 | 214 | And my react-ace code looks like: 215 | 216 | ```javascript 217 | import React, { Component } from "react"; 218 | 219 | import AceEditor from "react-ace"; 220 | import CustomSqlMode from "./CustomSqlMode.js"; 221 | 222 | import "ace-builds/src-noconflict/theme-github"; 223 | 224 | class App extends Component { 225 | componentDidMount() { 226 | const customMode = new CustomSqlMode(); 227 | this.refs.aceEditor.editor.getSession().setMode(customMode); 228 | } 229 | 230 | render() { 231 | return ( 232 |
233 | 240 |
241 | ); 242 | } 243 | } 244 | 245 | export default App; 246 | ``` 247 | -------------------------------------------------------------------------------- /docs/Migrate-v7-to-v8.md: -------------------------------------------------------------------------------- 1 | 2 | ## 1) Uninstall `brace` and install `ace-builds` 3 | 4 | ```sh 5 | npm uninstall brace 6 | npm install react-ace@8.0.0 ace-builds 7 | ``` 8 | 9 | ## 2) migrate modes and themes 10 | 11 | For example replace 12 | 13 | ```js 14 | import 'brace/mode/html' 15 | import 'brace/theme/monokai' 16 | import 'brace/snippets/html' 17 | ``` 18 | 19 | with 20 | 21 | ```js 22 | import 'ace-builds/src-noconflict/mode-html' 23 | import 'ace-builds/src-noconflict/theme-monokai' 24 | import 'ace-builds/src-noconflict/snippets/html' 25 | ``` 26 | 27 | ## 3) You may need to configure the ace-build workers 28 | 29 | See the discussion here: https://github.com/securingsincity/react-ace/issues/725 30 | 31 | If autocomplete or validation features are not working or you see an error message in the console regarding the ace-build worker, you may need to configure it to load properly using webpack or point to a CDN copy of the worker. 32 | -------------------------------------------------------------------------------- /docs/Modes.md: -------------------------------------------------------------------------------- 1 | # Modes, Themes, and Keyboard Handlers 2 | 3 | All modes, themes, and keyboard handlers should be required through [`ace-builds`](https://github.com/ajaxorg/ace-builds) directly. 4 | 5 | ### Example Modes 6 | 7 | - javascript 8 | - java 9 | - python 10 | - xml 11 | - ruby 12 | - sass 13 | - markdown 14 | - mysql 15 | - json 16 | - html 17 | - handlebars 18 | - golang 19 | - csharp 20 | - coffee 21 | - css 22 | 23 | ### Example Themes 24 | 25 | - monokai 26 | - github 27 | - tomorrow 28 | - kuroir 29 | - twilight 30 | - xcode 31 | - textmate 32 | - solarized dark 33 | - solarized light 34 | - terminal 35 | 36 | ### Example Keyboard Handlers 37 | 38 | - vim 39 | - emacs 40 | -------------------------------------------------------------------------------- /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 | http://securingsincity.github.io/react-ace/split.html 8 | 9 | ## Example Code 10 | 11 | ```javascript 12 | import React from "react"; 13 | import { render } from "react-dom"; 14 | 15 | import { split as SplitEditor } from "react-ace"; 16 | 17 | import "ace-builds/src-noconflict/mode-java"; 18 | import "ace-builds/src-noconflict/theme-github"; 19 | 20 | // Render editor 21 | render( 22 | , 31 | document.getElementById("example") 32 | ); 33 | ``` 34 | 35 | ## Available Props 36 | 37 | | Prop | Default | Type | Description | 38 | | ------------------------- | ------------ | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 39 | | name | 'ace-editor' | String | Unique Id to be used for the editor | 40 | | mode | '' | String | Language for parsing and code highlighting | 41 | | splits | 2 | Number | Number of views to have | 42 | | orientation | 'beside' | String | The orientation of the splits either `beside` or `below` | 43 | | theme | '' | String | theme to use | 44 | | value | '' | Array of Strings | value you want to populate in each code editor | 45 | | defaultValue | '' | Array of Strings | Default value for each editor | 46 | | height | '500px' | String | CSS value for height | 47 | | width | '500px' | String | CSS value for width | 48 | | className | | String | custom className | 49 | | fontSize | 12 | Number | pixel value for font-size | 50 | | showGutter | true | Boolean | show gutter | 51 | | showPrintMargin | true | Boolean | show print margin | 52 | | highlightActiveLine | true | Boolean | highlight active line | 53 | | focus | false | Boolean | whether to focus | 54 | | cursorStart | 1 | Number | the location of the cursor | 55 | | wrapEnabled | false | Boolean | Wrapping lines | 56 | | readOnly | false | Boolean | make the editor read only | 57 | | minLines | | Number | Minimum number of lines to be displayed | 58 | | maxLines | | Number | Maximum number of lines to be displayed | 59 | | enableBasicAutocompletion | false | Boolean | Enable basic autocompletion | 60 | | enableLiveAutocompletion | false | Boolean | Enable live autocompletion | 61 | | tabSize | 4 | Number | tabSize | 62 | | debounceChangePeriod | null | Number | A debounce delay period for the onChange event | 63 | | onLoad | | Function | called on editor load. The first argument is the instance of the editor | 64 | | onBeforeLoad | | Function | called before editor load. the first argument is an instance of `ace` | 65 | | onChange | | Function | occurs on document change it has 2 arguments the value of each editor and the event. | 66 | | onCopy | | Function | triggered by editor `copy` event, and passes text as argument | 67 | | onPaste | | Function | Triggered by editor `paste` event, and passes text as argument | 68 | | 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 | 69 | | 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 | 70 | | onFocus | | Function | triggered by editor `focus` event | 71 | | onBlur | | Function | triggered by editor `blur` event | 72 | | onInput | | Function | triggered by editor `input` event | 73 | | onScroll | | Function | triggered by editor `scroll` event | 74 | | editorProps | | Object | properties to apply directly to the Ace editor instance | 75 | | setOptions | | Object | [options](https://github.com/ajaxorg/ace/wiki/Configuring-Ace) to apply directly to the Ace editor instance | 76 | | keyboardHandler | | String | corresponding to the keybinding mode to set (such as vim or emacs) | 77 | | commands | | Array | new commands to add to the editor | 78 | | 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 | 79 | | 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' }]` | 80 | | style | | Object | camelCased properties | 81 | -------------------------------------------------------------------------------- /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/index"; 4 | 5 | import "ace-builds/src-noconflict/mode-jsx"; 6 | import "ace-builds/src-min-noconflict/ext-searchbox"; 7 | import "ace-builds/src-min-noconflict/ext-language_tools"; 8 | import "ace-builds/src-noconflict/theme-github"; 9 | const defaultValue = [ 10 | `// Use this tool to display differences in code. 11 | // Deletions will be highlighted on the left, insertions highlighted on the right.`, 12 | `// Use this too to show difference in code. 13 | // Deletions will be highlighted on the left, insertions highlighted on the right. 14 | // The diff highlighting style can be altered in CSS. 15 | ` 16 | ]; 17 | 18 | const languages = [ 19 | "javascript", 20 | "java", 21 | "python", 22 | "xml", 23 | "ruby", 24 | "sass", 25 | "markdown", 26 | "mysql", 27 | "json", 28 | "html", 29 | "handlebars", 30 | "golang", 31 | "csharp", 32 | "elixir", 33 | "typescript", 34 | "css" 35 | ]; 36 | 37 | languages.forEach(lang => { 38 | require(`ace-builds/src-noconflict/mode-${lang}`); 39 | require(`ace-builds/src-noconflict/snippets/${lang}`); 40 | }); 41 | 42 | class App extends Component { 43 | constructor(props) { 44 | super(props); 45 | this.state = { 46 | value: defaultValue, 47 | fontSize: 14, 48 | markers: {}, 49 | mode: "javascript" 50 | }; 51 | this.onChange = this.onChange.bind(this); 52 | this.setMode = this.setMode.bind(this); 53 | } 54 | 55 | onChange(newValue) { 56 | this.setState({ 57 | value: newValue 58 | }); 59 | } 60 | 61 | setMode(e) { 62 | this.setState({ 63 | mode: e.target.value 64 | }); 65 | } 66 | 67 | render() { 68 | return ( 69 |
70 |
71 |
72 | 73 |

74 | 75 | 86 | 87 |

88 |
89 | 90 |
91 |
92 |
93 |

Editor

94 | 104 |
105 |
106 | ); 107 | } 108 | } 109 | 110 | render(, document.getElementById("example")); 111 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Document 6 | 7 | 8 | 9 |
10 |
11 |

React-Ace

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

131 | 132 | 143 | 144 |

145 |
146 | 147 |
148 | 149 |

150 | 151 | 162 | 163 |

164 |
165 | 166 |
167 | 168 |

169 | 170 | 181 | 182 |

183 |
184 | 185 |
186 | 187 |

188 | 194 |

195 |
196 | 197 |
198 |

199 | 212 |

213 |
214 |
215 |

216 | 229 |

230 |
231 |
232 |

233 | 243 |

244 |
245 |
246 |

247 | 257 |

258 |
259 |
260 |

261 | 271 |

272 |
273 |
274 |

275 | 285 |

286 |
287 |
288 |

289 | 299 |

300 |
301 |
302 |
303 |

Editor

304 | 328 |
329 |
330 |

Code

331 | 354 | `} 355 | /> 356 |
357 |
358 | ); 359 | } 360 | } 361 | 362 | render(, document.getElementById("example")); 363 | -------------------------------------------------------------------------------- /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 "ace-builds/src-noconflict/mode-jsx"; 5 | import "ace-builds/src-min-noconflict/ext-searchbox"; 6 | import "ace-builds/src-min-noconflict/ext-language_tools"; 7 | 8 | const languages = [ 9 | "javascript", 10 | "java", 11 | "python", 12 | "xml", 13 | "ruby", 14 | "sass", 15 | "markdown", 16 | "mysql", 17 | "json", 18 | "html", 19 | "handlebars", 20 | "golang", 21 | "csharp", 22 | "elixir", 23 | "typescript", 24 | "css" 25 | ]; 26 | 27 | const themes = [ 28 | "monokai", 29 | "github", 30 | "tomorrow", 31 | "kuroir", 32 | "twilight", 33 | "xcode", 34 | "textmate", 35 | "solarized_dark", 36 | "solarized_light", 37 | "terminal" 38 | ]; 39 | 40 | languages.forEach(lang => { 41 | require(`ace-builds/src-noconflict/mode-${lang}`); 42 | require(`ace-builds/src-noconflict/snippets/${lang}`); 43 | }); 44 | 45 | themes.forEach(theme => require(`ace-builds/src-noconflict/theme-${theme}`)); 46 | 47 | const defaultValue = [ 48 | `function onLoad(editor) { 49 | console.log("i've loaded"); 50 | }`, 51 | 'const secondInput = "me i am the second input";' 52 | ]; 53 | class App extends Component { 54 | onLoad() { 55 | console.log("i've loaded"); 56 | } 57 | onChange(newValue) { 58 | console.log("change", newValue); 59 | this.setState({ 60 | value: newValue 61 | }); 62 | } 63 | 64 | onSelectionChange(newValue, event) { 65 | console.log("select-change", newValue); 66 | console.log("select-change-event", event); 67 | } 68 | 69 | onCursorChange(newValue, event) { 70 | console.log("cursor-change", newValue); 71 | console.log("cursor-change-event", event); 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 | setSplits(e) { 95 | this.setState({ 96 | splits: parseInt(e.target.value, 10) 97 | }); 98 | } 99 | setOrientation(e) { 100 | this.setState({ 101 | orientation: e.target.value 102 | }); 103 | } 104 | constructor(props) { 105 | super(props); 106 | this.state = { 107 | splits: 2, 108 | orientation: "beside", 109 | value: defaultValue, 110 | theme: "github", 111 | mode: "javascript", 112 | enableBasicAutocompletion: false, 113 | enableLiveAutocompletion: false, 114 | fontSize: 14, 115 | showGutter: true, 116 | showPrintMargin: true, 117 | highlightActiveLine: true, 118 | enableSnippets: false, 119 | showLineNumbers: true 120 | }; 121 | this.setTheme = this.setTheme.bind(this); 122 | this.setMode = this.setMode.bind(this); 123 | this.onChange = this.onChange.bind(this); 124 | this.setFontSize = this.setFontSize.bind(this); 125 | this.setBoolean = this.setBoolean.bind(this); 126 | this.setSplits = this.setSplits.bind(this); 127 | this.setOrientation = this.setOrientation.bind(this); 128 | } 129 | render() { 130 | return ( 131 |
132 |
133 |
134 | 135 |

136 | 137 | 148 | 149 |

150 |
151 | 152 |
153 | 154 |

155 | 156 | 167 | 168 |

169 |
170 | 171 |
172 | 173 |

174 | 175 | 186 | 187 |

188 |
189 | 190 |
191 | 192 |

193 | 194 | 205 | 206 |

207 |
208 | 209 |
210 | 211 |

212 | 213 | 224 | 225 |

226 |
227 |
228 |

229 | 242 |

243 |
244 |
245 |

246 | 259 |

260 |
261 |
262 |

263 | 273 |

274 |
275 |
276 |

277 | 287 |

288 |
289 |
290 |

291 | 301 |

302 |
303 |
304 |

305 | 315 |

316 |
317 |
318 |

319 | 329 |

330 |
331 |
332 |
333 |

Editor

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