├── .gitignore ├── .jshintrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bower.json ├── demo ├── demo.css ├── demo.html └── demo.js ├── gruntFile.js ├── package.json ├── publish.js ├── src └── ui-ace.js └── test ├── ace.spec.js └── karma.conf.js /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components/ 2 | node_modules/ 3 | dist/ 4 | out/ 5 | coverage/ 6 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "boss": true, 3 | "browser": true, 4 | "eqnull": true, 5 | "expr": true, 6 | "globalstrict": true, 7 | "immed": true, 8 | "laxbreak": true, 9 | "loopfunc": true, 10 | "newcap": true, 11 | "noarg": true, 12 | "noempty": true, 13 | "nonew": true, 14 | "quotmark": true, 15 | "smarttabs": true, 16 | "sub": true, 17 | "trailing": true, 18 | "undef": true, 19 | "unused": true, 20 | "globals": { 21 | "angular": false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | git: 3 | depth: 1 4 | language: node_js 5 | node_js: 6 | - '0.10' 7 | before_install: 8 | - export CHROME_BIN=chromium-browser 9 | - export DISPLAY=:99.0 10 | - sh -e /etc/init.d/xvfb start 11 | - npm update -g 12 | before_script: 13 | - npm install -qg bower grunt-cli 14 | - bower install 15 | after_success: 16 | - "./node_modules/angular-ui-publisher/travis/authentication.sh || exit 0" 17 | - "grunt dist build:gh-pages publish:gh-pages build:bower publish:bower" 18 | env: 19 | global: 20 | - REPO="git@github.com:angular-ui/ui-ace.git" 21 | - secure: ! 'VLH/q8Pk4ctxDAyXJ8py5H8ebfjgHPr7yGjGq3ktmKX7QPAVaeQ8RLfWgYsJ 22 | 23 | lJb8MJoorbh4gmjNnN2hooy++PoosIF9xXNyHASnfvW17cT/Ty450EQ8W0Mq 24 | 25 | 5b24H/YO58BcDnGXhJZjyiCQxD/7RqA93WvFA/TeCDwhfFsT7I0=' 26 | - secure: ! 'TBUTlinCENkifp/Q2pO/dO05TpH47f2XfTcweo4AdPxb0wREqasf1uJZbfe6 27 | 28 | +i9UgtkbeLn23qUeejkROhhNj9spuwd9k3MZ/qcoESshUXQj4QSiylukmJux 29 | 30 | Mn374o0UoCoTDe533KbSCS6Gv1RYeHXlACnMN20u/efYLYbmkW4=' 31 | - secure: ! 'f2a2Dw3NBwDULKIjzT73U8GNBx4K40dnHguH3oWwgQFjadImhirhUAqFH6N3 32 | 33 | 11GucoO8NQzZHvHJK4xM/1ZQh1JAqBjWCNOxdSZ81mlTa63L9/Lb5olsq3IV 34 | 35 | c5Lj/0xNWs8uaOBJWj2y/UCzHI/oN7XU0Qwnw/eDB4zNAFYnMuY=' 36 | - secure: ! 'UOPDi1c8u2Tcq2kIRsVvC2bTPXepXoZnPBZFVZLPKOXjMfTd6YltU8ax4OE9 37 | 38 | et91q+DXgedscfsRk+0+S3UthKMUKx7bsUPoTOE1ZQ8r/t4NUmrxCuL7Sh6s 39 | 40 | pFElhbYDrE/cZKP9d+RaiP8gQrZLsEGiRSeDwiccHJ9UqbwaX9g=' 41 | - secure: ! 'TO6Lt1P6Ovz56YlPLJEduFXWsEYk6vQ1RnK3b1tX2NNcyvPg0i9+yLa/gtvG 42 | 43 | TBJjh3lUz5osRwEKOkrNhz3n5wX6IPUXUpsO/RXSuWWyqkp2HQnF5h0V1uzw 44 | 45 | 79Ph+ZhSGla1h9RFqe+Qu7uXt56p8XGCLf0wcHpcy4qAS24ghbM=' 46 | - secure: ! 'ROX/ZxF62Mh0Ibepek+tLJp6YKOcqj8nu7dxlIDZmEOk17oZvU3kY5EH+5B8 47 | 48 | nvF3htGs/fLKE/RmAMPXsN+BlCq810a0oP+cjvNG+3lasxhjnBOQ7W7JrEWn 49 | 50 | OuPsmNOoBPekpcBh6MNHQXKrZAeCrJ29vIlBjKZxANGi/w6nHww=' 51 | - secure: ! 'B3ZuO6PejsK6fyFuqp9BGVuDva8GYRxJyx4WUTXxz+BsxgRm5GXon3L7caXY 52 | 53 | jffI2MkglsqCulXn2ckfq1O2KPEU41Gp9uLZJCDIMuSz+ItCXuSf4mdx0YLr 54 | 55 | P8bNPN5FwHwbXw/MI+LfF6LT+8Fm2Dj5gVl6aKW6+2/uKH8dEj0=' 56 | - secure: ! 'I3eyfTEtxHKmEcOA/27h9C+nFLadrC3N3PQn1Owrb+zngFzCLF/NkTxkBA1v 57 | 58 | UiNHaB6RL08i2qJAmtpcFKHFZy+q/qaMJeaVN4rVOrOi7xHRbxTOSMp0IwyC 59 | 60 | Z8wnHQosbsMt6lYQ8CF9W7qyOfBCcUEk3R++jWRuX0CHXMyRldY=' 61 | - secure: ! 'MYeCpdwZQy79+Rv3JnaI5lpuVHg5fGMRzOTpSNFE6TREgihX85s8YRH+j/ru 62 | 63 | 63mM7GHKZiTMQgblVJ073mxEzIGWwMq4+xpcQjlBcRXgb01e7tVpcj7wZe7f 64 | 65 | pLqZ7igWBFrlaRMTDfmH6JdjkZl80vGQQiBZdhzrinoWcxx+r5o=' 66 | - secure: ! 'O74deugKvWkpHTqdTjwEtX3JcESIMGMxOKwzuTOKsHLOJWbs0VqUrbRcn8Zc 67 | 68 | C+gmybRF+eTcsdvd2g/1LQFmY47F20j1pOBOpzD9cLw/iFbibUF+j7fXGVhH 69 | 70 | I5hQQKoPm9Ql72kO8H4cyK2UDGceXWaS+iut69BSiI3vAp0n5DA=' 71 | - secure: ! 'QBC1ePYxDDp4Y3pAZ93VYbPni3qq0TS80yC/9aDCGZftz6wDELlGRcZ8lmgE 72 | 73 | iFbkRGUcKmRmYuMweD/nWot/eXa7U3Gt6oeQHANlwwOjq+ggtDY3xGpPM2IP 74 | 75 | z4upux00abx8Iu1mdmdWoGnungCrwNna0C5Om8L+y4ItAv9s2K8=' 76 | - secure: ! 'JbA01+Mky7LkH9FiRet02dd2x6V6K2glJjFByOcBit4TJ2qlJhjej2Pmb98B 77 | 78 | Szb71hz/olxaeocTJj3kSC6JTL6A692wXg/qkCa+Ns8+ZPLXZax2CMc0ROAF 79 | 80 | 3Q+WrUG6M804LqAqIyV7GLQInY+yVrQ9Q4ZQL9HM4KYxTMz24M8=' 81 | - secure: ! 'QEAGBuplaQtdWAtT2LgrPaovq2XwOHKyLuNCOX15xR3lH+uP66Lu2sbo94X4 82 | 83 | tmgL0os+6pOpLN8IsDufBAqgPuL8/VY36Zh6pOz5QQ8EOGOpBRZ/UxITcLlt 84 | 85 | IoII/b1nWZspAV+o3J6ZsTvwGhj4UB6xa7wtxPDZPuQZE3r+o64=' 86 | - secure: ! 'ajSiUYBFIL62qQvct9+fPlKS3RhLP+Bwe2mAQ72SNPW+qu3kPdpJjHzbsSuA 87 | 88 | KprhwGDXfDMc9qyOXEaRptmcm263Sur8+1rzI4IWJvgbk4p7yWTKRtnCAi7+ 89 | 90 | rvVlw4ob3PjNaYPcbpX4x/Emt6BDoRt0vFYmGjR0uDdsmaXBJgI=' 91 | - secure: ! 'cesPxKxMwhqIF2GOJHB3kJJ0QKyR6bsz+p8rv7BeOf/it/mIq2NFZUdSy7IN 92 | 93 | uEJgCvkx8fbf0nIgk1z2q6qI5nk7NYR/ukVyfKwFXViD7s2Jp0Uraeo3HWo1 94 | 95 | ktQBKoE+dDb+fDc0dnnIF5ObMBiP1TezwAjUHxTp91NltMOm6RY=' 96 | - secure: ! 'geWYiP5Lfl4D1ClaQBgPrqHlVjALdM4huBOsxW32f2J9btfqceNrr8VSJ5lq 97 | 98 | +FGxE1uaScG2APz6Di6jDIs9UAoG0uaSYVaMf/O5M/VT5zX3C/uUFe0SRVjX 99 | 100 | XTw8cTOdm50C0ePNr9a47DTtMC9qZbXY++6Zj+KYeqep1M5xfkg=' 101 | - secure: ! 'DhuiLpfuZRFGtLW+f3g5S5i7VME0cqC6Zvmg2njBrkQytTX7wJZr6Oq8p5Az 102 | 103 | /1IB5y4AG2RLiTakP9YpkGVoqcIecCNY3/mGsoAi4sedA/I+Qcjn1jGi7YVm 104 | 105 | rJq1DLloTIpJBtjzjqBpO13kPR/RPGAoHAnOF/pIkiVZHpOgk5c=' 106 | - secure: ! 'IpPbGbS17dU7XcBNSqex8M2eTdjsQkaxV36Jb4Cc6rm49rp6yddyNeK88FOH 107 | 108 | HP4r3Cf7ANv/mI1DzMM94QBGYq9bXcA3RarSZeqCtjzQ4Z9v9hkE9vWR8bGy 109 | 110 | rMsXc/kRIkUvrw9pVc4UCKkqgwJax+WKbLMatIEVYeCTn0Nnprk=' 111 | - secure: ! 'gpDRE0U7rNPZ5VIGPZ9v6dq0hmXdyqrwW5Uf26OWkI21Brpbntx6m1cksGWH 112 | 113 | 4/MT+HHacMDvnw/hFUsXG3WZKpwx6s9wh6QYERo842Vb6WlyDczCypcToQHW 114 | 115 | jzgm773MYyZ6aqF+9Z+owxFWvGdi1dLfinUuSa1pAZGLvKtkjHY=' 116 | - secure: ! 'GNTtN5gLvJGB5GuR+lAVqpF46Hv+cUmS97V6e8mibhWvuJHp68Quu0AUF7c7 117 | 118 | HCk70lhGB+IR9voqjgmMmu1m5a58VJwoiqlt7lyXkhauh/J5xe2OSkYD0mBa 119 | 120 | 6IwP/XyFBalW7rhaQhH5VL6MO+S/gdNJ7Thb3z39IMC8oSDGY+8=' 121 | - secure: ! 'fPrBq24xYHsQ5YsDX4HksoatIQnhw4PssD/jb4VAEtPA2Vw0eJKaBquBwoVU 122 | 123 | xhpsMmr7I1OfjPaavldMFFBH+/uMnKANQWxTrZTJ1zr8Yphsha46DuqOH60/ 124 | 125 | w6NguZE7Ipdx+VfDA6WQFYGtowI02oKzWjshCrndWYw9Q7icx9o=' 126 | - secure: ! 'T0f9CyHOo2y/UIk6BBmxd7cMKNYp9/FyQTfJP3YE3GVbdeZAJg4GyiD2kVWP 127 | 128 | 89kcUUoOz2MM79XB77tuH+u7I3U1HXaWwbCvxo6S5a5nn2/Y4ae06QmvcU+7 129 | 130 | Iiv9n1uLtPdxQyTBRP0Tg1TsCWVXF0/FxxnAEvTUPlraoLLzn/Q=' 131 | - secure: ! 'TCxSdqLdgNFHAzscEWDTMkLTQqBh26kEsEiU3cvhbczsgIqIHPlEvT7U5+1k 132 | 133 | O1d+URqsH7560aDN5Slt1FeCQDmDZtNe+RxMDlw2HnnrZXOGDjUpUvtcZDif 134 | 135 | TZ/1xM4ZIoU/w2K0qCmTZsVm/VoNrmUrH8N+uCxH4rmgvRqD0aU=' 136 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ### 0.2.3 (2015-01-29) 3 | 4 | 5 | #### Bug Fixes 6 | 7 | * another change to evalAsync 8 | 9 | 10 | ### 0.2.2 (2015-01-27) 11 | 12 | 13 | #### Bug Fixes 14 | 15 | * onLoad called in the middle of the options ([5b1a3170](http://github.com/angular-ui/ui-ace/commit/5b1a3170acd482ded223cc21f31fcbf62964f4d4)) 16 | * change to evalAsync 17 | 18 | 19 | ### 0.2.1 (2015-01-11) 20 | 21 | 22 | #### Bug Fixes 23 | 24 | * bad user callback digestion ([df526049](http://github.com/angular-ui/ui-ace/commit/df5260499ee83acd14fd2fd513f5fc19cec51f11)) 25 | 26 | 27 | 28 | ### 0.2.0 (2015-01-08) 29 | 30 | 31 | #### Bug Fixes 32 | 33 | * ngModel put back the hack ([415610f0](http://github.com/angular-ui/ui-ace/commit/415610f0dcdc1116267e6aa4045bd5ae356f1fdc)) 34 | * ngModel update ([491369a9](http://github.com/angular-ui/ui-ace/commit/491369a9d508c28ad880ad707e35c3d827c0a71a), closes [#77](http://github.com/angular-ui/ui-ace/issues/77)) 35 | 36 | 37 | #### Features 38 | 39 | * readonly ng ready ([0d99e5fd](http://github.com/angular-ui/ui-ace/commit/0d99e5fd65a9870617b9010111b72f789e276c8b)) 40 | * add advancedRenderer options ([76fc37d1](http://github.com/angular-ui/ui-ace/commit/76fc37d176ffe1e62dc72da4e85c027446794a64)) 41 | * **config:** always call the global onLoad handler ([c83086ac](http://github.com/angular-ui/ui-ace/commit/c83086ac7be7ff08dd632ab6705c43ba39ca6867)) 42 | * **directive:** 43 | * worker path for concatenated and minified configurations ([b7e20c1a](http://github.com/angular-ui/ui-ace/commit/b7e20c1aa6c4871a62b48fbb36a7a6794809d2b0)) 44 | * require and advanced options ([6e160cb6](http://github.com/angular-ui/ui-ace/commit/6e160cb6f12b11365e8480509332951ea43d7e5a)) 45 | 46 | 47 | 48 | ### 0.1.2 (2015-01-08) 49 | 50 | 51 | 52 | ### 0.1.1 (2015-01-08) 53 | 54 | 55 | #### Bug Fixes 56 | 57 | * **ace:** resize editor when its width/height changes ([b2024c14](http://github.com/angular-ui/ui-ace/commit/b2024c14dc6336bc232e1bb0b124b6be26f2bbee), closes [#26](http://github.com/angular-ui/ui-ace/issues/26)) 58 | 59 | 60 | 61 | ## v0.1.0 (2013-12-28) 62 | 63 | 64 | #### Bug Fixes 65 | 66 | * **publisher:** remove typo ([15a99e22](http://github.com/angular-ui/ui-ace/commit/15a99e22d4b761845abc7e7644b88d7eb45ee538)) 67 | * **travis:** 68 | * use node 0.10 ([588f6f1f](http://github.com/angular-ui/ui-ace/commit/588f6f1fc6fbca76b82db4e1e1c0a1d34d2a9835)) 69 | * use angular-ui-publisher ([126de946](http://github.com/angular-ui/ui-ace/commit/126de946574f857919010bf3e2f7e46f52629b23)) 70 | * **ui-ace:** call $destroy when removed ([edb4fa14](http://github.com/angular-ui/ui-ace/commit/edb4fa149b8d2c9dbb7314a69e5d58dbc688fc0d)) 71 | 72 | 73 | ### v0.0.5 (2013-12-14) 74 | 75 | 76 | #### Bug Fixes 77 | 78 | * **demo:** Correct indentation. ([3482eaa2](http://github.com/angular-ui/ui-ace/commit/3482eaa2b570e6818e652d4ce116974511b8732c)) 79 | * **travis:** 80 | * Run bower install twice to make sure it does ([6c909f6b](http://github.com/angular-ui/ui-ace/commit/6c909f6b444f7d1ce67f1c4e7e0245ba83c75700)) 81 | * Travis scripts bug... ([bbae258e](http://github.com/angular-ui/ui-ace/commit/bbae258e30c8a87fa3694422207b20a750729177)) 82 | * **ui-ace:** issue with digest already in progress error ([1b2dcd51](http://github.com/angular-ui/ui-ace/commit/1b2dcd516e430915f698d574513f93bb1bce4b68)) 83 | 84 | 85 | #### Features 86 | 87 | * **demo:** 88 | * Add more demos ! ([5a2be000](http://github.com/angular-ui/ui-ace/commit/5a2be000fae6936a5a260e636feb9674de2e782e)) 89 | * Add a demo.css file ([83bbacd5](http://github.com/angular-ui/ui-ace/commit/83bbacd54cbabb7eb7a9b072f09c589251db57c7)) 90 | * Add angular-ui-bootstrap-bower for tabset directive. ([df6a1b67](http://github.com/angular-ui/ui-ace/commit/df6a1b67067105220e7e4b0b600275653a97f47a)) 91 | * **test:** add test code for the readOnly option ([5da05ab7](http://github.com/angular-ui/ui-ace/commit/5da05ab7b3f8a45f06cdc0e19655e7efcffdeb64)) 92 | * **ui-ace:** 93 | * Make the readonly option use double-curly expressions. Close #3. ([2115d615](http://github.com/angular-ui/ui-ace/commit/2115d61529bd4f9ec4db4f95a414ebd2396ef7ad)) 94 | * add readOnly option ([cffd1e45](http://github.com/angular-ui/ui-ace/commit/cffd1e454ebcf24ebafa18830e20ab8ef4f5c27e)) 95 | 96 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2012 the AngularUI Team, http://angular-ui.github.com 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UI.Ace directive [![Build Status](https://travis-ci.org/angular-ui/ui-ace.svg)](https://travis-ci.org/angular-ui/ui-ace) 2 | 3 | This directive allows you to add [ACE](http://ajaxorg.github.io/ace/) editor elements. 4 | 5 | ## Requirements 6 | 7 | - AngularJS 8 | - [Ace 1.x](https://github.com/ajaxorg/ace-builds/) 9 | 10 | 11 | ## Usage 12 | 13 | You can get it from [Bower](http://bower.io/) 14 | 15 | ```sh 16 | bower install angular-ui-ace#bower 17 | ``` 18 | 19 | This will copy the UI.Ace files into a `bower_components` folder, along with its dependencies. Load the script files in your application: 20 | 21 | ```html 22 | 23 | 24 | 25 | ``` 26 | 27 | Add the UI.Ace module as a dependency to your application module: 28 | 29 | ```javascript 30 | var myAppModule = angular.module('MyApp', ['ui.ace']); 31 | ``` 32 | 33 | Finally, add the directive to your html: 34 | 35 | ```html 36 |
37 | ``` 38 | 39 | To see something it's better to add some CSS, like 40 | 41 | 42 | ```css 43 | .ace_editor { height: 200px; } 44 | ``` 45 | 46 | ## Options 47 | 48 | Ace doesn't provide a one gate access to all the options the jquery way. 49 | Each option is configured with the function of a specific instance. 50 | See the [api doc](http://ajaxorg.github.io/ace/#nav=api) for more. 51 | 52 | Although, _ui-ace_ automatically handles some handy options : 53 | + _showGutter_ : to show the gutter or not. 54 | + _useWrapMode_ : to set whether or not line wrapping is enabled. 55 | + _theme_ : to set the theme to use. 56 | + _mode_ : to set the mode to use. 57 | + _onLoad_ : callback when the editor has finished loading (see [below](#ace-instance-direct-access)). 58 | + _onChange_ : callback when the editor content is changed (). 59 | + _onBlur_ : callback when the editor is blurred (). 60 | + _firstLineNumber_ : to set the firstLineNumber (default: 1) 61 | 62 | ```html 63 |
72 | ``` 73 | 74 | You'll want to define the `onLoad` and the `onChange` callback on your scope: 75 | 76 | ```javascript 77 | myAppModule.controller('MyController', [ '$scope', function($scope) { 78 | 79 | $scope.aceLoaded = function(_editor) { 80 | // Options 81 | _editor.setReadOnly(true); 82 | }; 83 | 84 | $scope.aceChanged = function(e) { 85 | // 86 | }; 87 | 88 | }]); 89 | ``` 90 | 91 | To handle other options you'll have to use a direct access to the Ace created instance (see [below](#ace-instance-direct-access)). 92 | 93 | ## Advanced Options 94 | 95 | You can specify advanced options and even `require` options in the directive, as well. For this example, you 96 | will have to include the `ext-language_tools.js` file from the ace source code. 97 | 98 | This will copy the UI.Ace files into a `bower_components` folder, along with its dependencies. Load the script files in your application: 99 | 100 | ```html 101 | 102 | ``` 103 | 104 | ```html 105 |
113 | ``` 114 | 115 | To include options applicable to the ACE renderer, you can use the `rendererOptions` key: 116 | 117 | ```html 118 |
123 | ``` 124 | 125 | ## Support for concatenated bundles 126 | 127 | Trying to use ace with concatenated javascript files usually fails because it changes the physical location of the `workerPath`. If you 128 | need to work with bundled or minified versions of ace, you can specify the original location of the `workerPath` on disk (_not the bundled file_). 129 | 130 | This should be the folder on disk where `ace.js` resides. 131 | 132 | ```html 133 |
136 | ``` 137 | 138 | ### Working with ng-model 139 | 140 | The ui-ace directive plays nicely with ng-model. 141 | 142 | The ng-model will be watched for to set the Ace EditSession value (by [setValue](http://ajaxorg.github.io/ace/#nav=api&api=edit_session)). 143 | 144 | _The ui-ace directive stores and expects the model value to be a standard javascript String._ 145 | 146 | ### Can be read only 147 | 148 | Simple demo 149 | ```html 150 |
151 | or 152 | Check me to make Ace readonly:
153 |
154 | ``` 155 | 156 | ### Ace instance direct access 157 | 158 | For more interaction with the Ace instance in the directive, we provide a direct access to it. 159 | Using 160 | 161 | ```html 162 |
163 | ``` 164 | 165 | the `$scope.aceLoaded` function will be called with the [Ace Editor instance](http://ajaxorg.github.io/ace/#nav=api&api=editor) as first argument 166 | 167 | ```javascript 168 | myAppModule.controller('MyController', [ '$scope', function($scope) { 169 | 170 | $scope.aceLoaded = function(_editor){ 171 | // Editor part 172 | var _session = _editor.getSession(); 173 | var _renderer = _editor.renderer; 174 | 175 | // Options 176 | _editor.setReadOnly(true); 177 | _session.setUndoManager(new ace.UndoManager()); 178 | _renderer.setShowGutter(false); 179 | 180 | // Events 181 | _editor.on("changeSession", function(){ ... }); 182 | _session.on("change", function(){ ... }); 183 | }; 184 | 185 | }]); 186 | ``` 187 | 188 | ## Testing 189 | 190 | We use Karma and jshint to ensure the quality of the code. The easiest way to run these checks is to use grunt: 191 | 192 | ```sh 193 | npm install -g grunt-cli 194 | npm install && bower install 195 | grunt 196 | ``` 197 | 198 | The karma task will try to open Firefox and Chrome as browser in which to run the tests. Make sure this is available or change the configuration in `test\karma.conf.js` 199 | 200 | 201 | ### Grunt Serve 202 | 203 | We have one task to serve them all ! 204 | 205 | ```sh 206 | grunt serve 207 | ``` 208 | 209 | It's equal to run separately: 210 | 211 | * `grunt connect:server` : giving you a development server at [http://127.0.0.1:8000/](http://127.0.0.1:8000/). 212 | 213 | * `grunt karma:server` : giving you a Karma server to run tests (at [http://localhost:9876/](http://localhost:9876/) by default). You can force a test on this server with `grunt karma:unit:run`. 214 | 215 | * `grunt watch` : will automatically test your code and build your demo. You can demo generation with `grunt build:gh-pages`. 216 | 217 | 218 | ### Dist 219 | 220 | This repo is using the [angular-ui/angular-ui-publisher](https://github.com/angular-ui/angular-ui-publisher). 221 | New tags will automatically trigger a new publication. 222 | To test is locally you can trigger a : 223 | 224 | ```sh 225 | grunt dist build:bower 226 | ``` 227 | 228 | it will put the final files in the _'dist'_ folder and a sample of the bower tag output in the _'out/built/bower'_ folder. 229 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-ui-ace", 3 | "version": "0.2.3", 4 | "description": "This directive allows you to add ACE editor elements.", 5 | "author": "https://github.com/angular-ui/ui-ace/graphs/contributors", 6 | "license": "MIT", 7 | "homepage": "http://angular-ui.github.com", 8 | "main": "./src/ui-ace.js", 9 | "ignore": [ 10 | "**/.*", 11 | "node_modules", 12 | "bower_components", 13 | "test*", 14 | "demo*", 15 | "gruntFile.js", 16 | "package.json" 17 | ], 18 | "dependencies": { 19 | "angular": "~1.x", 20 | "ace-builds": "^1" 21 | }, 22 | "devDependencies": { 23 | "angular-mocks": "~1.x" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /demo/demo.css: -------------------------------------------------------------------------------- 1 | .ace_editor { 2 | height: 180px; 3 | } 4 | 5 | #demo-general .ace_editor { 6 | height: 250px; 7 | } 8 | 9 | #demo-mode-changing .ace_editor { 10 | height: 300px; 11 | } 12 | -------------------------------------------------------------------------------- /demo/demo.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 5 |
6 | 9 |
10 |
11 |
12 |
Ace here
13 |
14 |
15 | 16 |
21 |
22 | 25 |
26 | 27 | 28 | 29 |
30 |
<section>
 31 |   <div ui-ace >Ace here</div>
 32 | </section>
33 |
34 |
35 | 36 |
37 |
.ace_editor  {
 38 |   height : 200px;
 39 | }
 40 |   
41 |
42 |
43 |
44 |
45 |
46 |
47 | 48 | 50 |
51 | 54 |
55 |
56 |
57 |
# Theme and mode 63 | 64 | *Lorem ipsum* dolor sit amet, consectetur adipisicing elit, sed do eiusmod 65 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 66 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 67 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 68 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 69 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 70 |
71 |
72 |
73 | 74 |
79 |
80 | 83 |
84 | 85 | 86 | 87 |
88 |
<section>
 89 |     <div ui-ace="{
 90 |       useWrapMode : true,
 91 |       showGutter: false,
 92 |       theme:'twilight',
 93 |       mode: 'markdown'
 94 |     }" ># Theme and mode
 95 | 
 96 | *Lorem ipsum* dolor sit amet, consectetur adipisicing elit, sed do eiusmod
 97 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
 98 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
 99 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
100 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
101 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</div>
102 | 
103 | </section>
104 |
105 |
106 | 107 |
108 |
.ace_editor  {
109 |   height : 200px;
110 | }
111 | 
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | 120 | 122 |
123 | 126 | 127 |
128 |
129 |
Ace here
130 |
131 |
132 | Mode : 133 |
134 |
135 | 136 |
137 | 138 |
143 |
144 | 147 |
148 | 149 | 150 | 151 |
152 |
<section ng-controller="AceCtrl">
153 | 
154 |   <div ui-ace="aceOption" ng-model="aceModel"></div>
155 | 
156 |   <select ng-model="mode" ng-options="m for m in modes" ng-change="modeChanged()"></select>
157 | 
158 | </section>
159 |
160 |
161 |
162 | 163 |
164 |
function AceCtrl($scope) {
165 | // The modes
166 |   $scope.modes = ['Scheme', 'XML', 'Javascript'];
167 |   $scope.mode = $scope.modes[0];
168 | 
169 | 
170 |   // The ui-ace option
171 |   $scope.aceOption = {
172 |     mode: $scope.mode.toLowerCase(),
173 |     onLoad: function (_ace) {
174 | 
175 |       // HACK to have the ace instance in the scope...
176 |       $scope.modeChanged = function () {
177 |         _ace.getSession().setMode("ace/mode/" + $scope.mode.toLowerCase());
178 |       };
179 | 
180 |     }
181 |   };
182 | 
183 |   // Initial code content...
184 |   $scope.aceModel = ';; Scheme code in here.\n' +
185 |     '(define (double x)\n\t(* x x))\n\n\n' +
186 |     '<!-- XML code in here. -->\n' +
187 |     '<root>\n\t<foo>\n\t</foo>\n\t<bar/>\n</root>\n\n\n' +
188 |     '// Javascript code in here.\n' +
189 |     'function foo(msg) {\n\tvar r = Math.random();\n\treturn "" + r + " : " + msg;\n}';
190 | 
191 | }
192 |
193 |
194 | 195 |
196 |
.ace_editor  {
197 |   height : 300px;
198 | }
199 | 
200 |
201 |
202 |
203 |
204 |
205 |
206 | 207 | 208 |
209 | -------------------------------------------------------------------------------- /demo/demo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('doc.ui-ace', ['ui.ace', 'prettifyDirective', 'ui.bootstrap', 'plunker']) 4 | .controller('AceCtrl', ['$scope', function ($scope) { 5 | 6 | // The modes 7 | $scope.modes = ['Scheme', 'XML', 'Javascript']; 8 | $scope.mode = $scope.modes[0]; 9 | 10 | 11 | // The ui-ace option 12 | $scope.aceOption = { 13 | mode: $scope.mode.toLowerCase(), 14 | onLoad: function (_ace) { 15 | 16 | // HACK to have the ace instance in the scope... 17 | $scope.modeChanged = function () { 18 | _ace.getSession().setMode('ace/mode/' + $scope.mode.toLowerCase()); 19 | }; 20 | 21 | } 22 | }; 23 | 24 | // Initial code content... 25 | $scope.aceModel = ';; Scheme code in here.\n' + 26 | '(define (double x)\n\t(* x x))\n\n\n' + 27 | '\n' + 28 | '\n\t\n\t\n\t\n\n\n\n' + 29 | '// Javascript code in here.\n' + 30 | 'function foo(msg) {\n\tvar r = Math.random();\n\treturn "" + r + " : " + msg;\n}'; 31 | 32 | }]) 33 | ; 34 | -------------------------------------------------------------------------------- /gruntFile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (grunt) { 4 | 5 | require('load-grunt-tasks')(grunt); 6 | 7 | // Default task. 8 | grunt.registerTask('default', ['jshint', 'karma:unit']); 9 | grunt.registerTask('serve', ['karma:continuous', 'dist', 'build:gh-pages', 'connect:continuous', 'watch']); 10 | grunt.registerTask('dist', ['ngAnnotate', 'uglify']); 11 | 12 | 13 | // HACK TO ACCESS TO THE COMPONENT-PUBLISHER 14 | function fakeTargetTask(prefix){ 15 | return function(){ 16 | 17 | if (this.args.length !== 1) return grunt.log.fail('Just give the name of the ' + prefix + ' you want like :\ngrunt ' + prefix + ':bower'); 18 | 19 | var done = this.async(); 20 | var spawn = require('child_process').spawn; 21 | spawn('./node_modules/.bin/gulp', [ prefix, '--branch='+this.args[0] ].concat(grunt.option.flags()), { 22 | cwd : './node_modules/angular-ui-publisher', 23 | stdio: 'inherit' 24 | }).on('close', done); 25 | }; 26 | } 27 | 28 | grunt.registerTask('build', fakeTargetTask('build')); 29 | grunt.registerTask('publish', fakeTargetTask('publish')); 30 | 31 | // Project configuration. 32 | grunt.initConfig({ 33 | pkg: grunt.file.readJSON('package.json'), 34 | meta: { 35 | banner: ['/**', 36 | ' * <%= pkg.name %> - <%= pkg.description %>', 37 | ' * @version v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>', 38 | ' * @link <%= pkg.homepage %>', 39 | ' * @license <%= pkg.license %>', 40 | ' */', 41 | ''].join('\n') 42 | }, 43 | 44 | watch: { 45 | src: { 46 | files: ['src/*'], 47 | tasks: ['karma:unit:run', 'dist', 'build:gh-pages'] 48 | }, 49 | test: { 50 | files: ['test/*.js'], 51 | tasks: ['karma:unit:run'] 52 | }, 53 | demo: { 54 | files: ['demo/*', 'publish.js'], 55 | tasks: ['build:gh-pages'] 56 | }, 57 | livereload: { 58 | files: ['out/built/gh-pages/**/*'], 59 | options: { livereload: true } 60 | } 61 | }, 62 | 63 | karma: { 64 | unit: {configFile: 'test/karma.conf.js', singleRun: true}, 65 | coverage : { 66 | configFile: 'test/karma.conf.js', 67 | reporters: ['progress', 'coverage'], 68 | preprocessors: { 'src/*.js': ['coverage'] }, 69 | coverageReporter: { type : 'html', dir : 'coverage/' }, 70 | singleRun: true 71 | }, 72 | server: {configFile: 'test/karma.conf.js'}, 73 | continuous: {configFile: 'test/karma.conf.js', background: true } 74 | }, 75 | 76 | connect: { 77 | options: { 78 | base : 'out/built/gh-pages', 79 | open: true, 80 | livereload: true 81 | }, 82 | server: { options: { keepalive: true } }, 83 | continuous: { options: { keepalive: false } } 84 | }, 85 | 86 | 87 | jshint: { 88 | src: { 89 | files:{ src : ['src/*.js', 'demo/**/*.js'] }, 90 | options: { jshintrc: '.jshintrc' } 91 | }, 92 | test: { 93 | files:{ src : [ 'test/*.spec.js', 'gruntFile.js'] }, 94 | options: grunt.util._.extend({}, grunt.file.readJSON('.jshintrc'), { 95 | node: true, 96 | globals: { 97 | angular: false, 98 | inject: false, 99 | jQuery: false, 100 | 101 | jasmine: false, 102 | afterEach: false, 103 | beforeEach: false, 104 | ddescribe: false, 105 | describe: false, 106 | expect: false, 107 | iit: false, 108 | it: false, 109 | spyOn: false, 110 | xdescribe: false, 111 | xit: false 112 | } 113 | }) 114 | } 115 | }, 116 | 117 | uglify: { 118 | options: {banner: '<%= meta.banner %>'}, 119 | build: { 120 | expand: true, 121 | cwd: 'dist', 122 | src: ['*.js'], 123 | ext: '.min.js', 124 | dest: 'dist' 125 | } 126 | }, 127 | 128 | ngAnnotate: { 129 | main: { 130 | expand: true, 131 | cwd: 'src', 132 | src: ['*.js'], 133 | dest: 'dist' 134 | } 135 | }, 136 | 137 | changelog: { 138 | options: { 139 | dest: 'CHANGELOG.md', 140 | from: grunt.option('from') 141 | } 142 | } 143 | }); 144 | 145 | }; 146 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-ui-ace", 3 | "version": "0.2.3", 4 | "description": "This directive allows you to add ACE editor elements.", 5 | "author": "https://github.com/angular-ui/ui-ace/graphs/contributors", 6 | "license": "MIT", 7 | "homepage": "http://angular-ui.github.com", 8 | "main": "./ui-ace.js", 9 | "dependencies": {}, 10 | "devDependencies": { 11 | "angular-ui-publisher": "~1.x", 12 | "grunt": "~0.4.5", 13 | "grunt-contrib-connect": "~0.9.0", 14 | "grunt-contrib-copy": "~0.7.0", 15 | "grunt-contrib-jshint": "~0.10.0", 16 | "grunt-contrib-uglify": "~0.7.0", 17 | "grunt-contrib-watch": "~0.6.1", 18 | "grunt-conventional-changelog": "~1.1.0", 19 | "grunt-karma": "~0.9.0", 20 | "grunt-ng-annotate": "^0.8.0", 21 | "jasmine-core": "^2.1.3", 22 | "karma": "~0.12.31", 23 | "karma-chrome-launcher": "~0.1.7", 24 | "karma-coverage": "~0.2.7", 25 | "karma-firefox-launcher": "~0.1.4", 26 | "karma-jasmine": "~0.3.4", 27 | "karma-phantomjs-launcher": "~0.1.4", 28 | "load-grunt-tasks": "~2.0.0" 29 | }, 30 | "scripts": { 31 | "test": "grunt" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "git://github.com/angular-ui/ui-ace.git" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /publish.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | 3 | 'use strict'; 4 | 5 | var fs = require('fs'); 6 | var path = require('path'); 7 | 8 | module.exports = function() { 9 | 10 | var js_dependencies =[ 11 | 'bower_components/ace-builds/src-min-noconflict/ace.js', 12 | 'bower_components/ace-builds/src-min-noconflict/theme-twilight.js', 13 | 'bower_components/ace-builds/src-min-noconflict/mode-markdown.js', 14 | 'bower_components/ace-builds/src-min-noconflict/mode-scheme.js', 15 | 'bower_components/ace-builds/src-min-noconflict/worker-javascript.js' 16 | ]; 17 | 18 | function putThemInVendorDir (filepath) { 19 | return 'vendor/' + path.basename(filepath); 20 | } 21 | 22 | return { 23 | humaName : 'UI.Ace', 24 | repoName : 'ui-ace', 25 | inlineHTML : fs.readFileSync(__dirname + '/demo/demo.html'), 26 | inlineJS : fs.readFileSync(__dirname + '/demo/demo.js'), 27 | css: ['demo/demo.css'], 28 | js : js_dependencies.map(putThemInVendorDir).concat(['dist/ui-ace.min.js']), 29 | tocopy : js_dependencies, 30 | 31 | bowerData : { main: './ui-ace.js'} 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /src/ui-ace.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Binds a ACE Editor widget 5 | */ 6 | angular.module('ui.ace', []) 7 | .constant('uiAceConfig', {}) 8 | .directive('uiAce', ['uiAceConfig', function (uiAceConfig) { 9 | 10 | if (angular.isUndefined(window.ace)) { 11 | throw new Error('ui-ace need ace to work... (o rly?)'); 12 | } 13 | 14 | /** 15 | * Sets editor options such as the wrapping mode or the syntax checker. 16 | * 17 | * The supported options are: 18 | * 19 | * 26 | * 27 | * @param acee 28 | * @param session ACE editor session 29 | * @param {object} opts Options to be set 30 | */ 31 | var setOptions = function(acee, session, opts) { 32 | 33 | // sets the ace worker path, if running from concatenated 34 | // or minified source 35 | if (angular.isDefined(opts.workerPath)) { 36 | var config = window.ace.require('ace/config'); 37 | config.set('workerPath', opts.workerPath); 38 | } 39 | // ace requires loading 40 | if (angular.isDefined(opts.require)) { 41 | opts.require.forEach(function (n) { 42 | window.ace.require(n); 43 | }); 44 | } 45 | // Boolean options 46 | if (angular.isDefined(opts.showGutter)) { 47 | acee.renderer.setShowGutter(opts.showGutter); 48 | } 49 | if (angular.isDefined(opts.useWrapMode)) { 50 | session.setUseWrapMode(opts.useWrapMode); 51 | } 52 | if (angular.isDefined(opts.showInvisibles)) { 53 | acee.renderer.setShowInvisibles(opts.showInvisibles); 54 | } 55 | if (angular.isDefined(opts.showIndentGuides)) { 56 | acee.renderer.setDisplayIndentGuides(opts.showIndentGuides); 57 | } 58 | if (angular.isDefined(opts.useSoftTabs)) { 59 | session.setUseSoftTabs(opts.useSoftTabs); 60 | } 61 | if (angular.isDefined(opts.showPrintMargin)) { 62 | acee.setShowPrintMargin(opts.showPrintMargin); 63 | } 64 | 65 | // commands 66 | if (angular.isDefined(opts.disableSearch) && opts.disableSearch) { 67 | acee.commands.addCommands([ 68 | { 69 | name: 'unfind', 70 | bindKey: { 71 | win: 'Ctrl-F', 72 | mac: 'Command-F' 73 | }, 74 | exec: function () { 75 | return false; 76 | }, 77 | readOnly: true 78 | } 79 | ]); 80 | } 81 | 82 | // Basic options 83 | if (angular.isString(opts.theme)) { 84 | acee.setTheme('ace/theme/' + opts.theme); 85 | } 86 | if (angular.isString(opts.mode)) { 87 | session.setMode('ace/mode/' + opts.mode); 88 | } 89 | // Advanced options 90 | if (angular.isDefined(opts.firstLineNumber)) { 91 | if (angular.isNumber(opts.firstLineNumber)) { 92 | session.setOption('firstLineNumber', opts.firstLineNumber); 93 | } else if (angular.isFunction(opts.firstLineNumber)) { 94 | session.setOption('firstLineNumber', opts.firstLineNumber()); 95 | } 96 | } 97 | 98 | // advanced options 99 | var key, obj; 100 | if (angular.isDefined(opts.advanced)) { 101 | for (key in opts.advanced) { 102 | // create a javascript object with the key and value 103 | obj = { name: key, value: opts.advanced[key] }; 104 | // try to assign the option to the ace editor 105 | acee.setOption(obj.name, obj.value); 106 | } 107 | } 108 | 109 | // advanced options for the renderer 110 | if (angular.isDefined(opts.rendererOptions)) { 111 | for (key in opts.rendererOptions) { 112 | // create a javascript object with the key and value 113 | obj = { name: key, value: opts.rendererOptions[key] }; 114 | // try to assign the option to the ace editor 115 | acee.renderer.setOption(obj.name, obj.value); 116 | } 117 | } 118 | 119 | // onLoad callbacks 120 | angular.forEach(opts.callbacks, function (cb) { 121 | if (angular.isFunction(cb)) { 122 | cb(acee); 123 | } 124 | }); 125 | }; 126 | 127 | return { 128 | restrict: 'EA', 129 | require: '?ngModel', 130 | link: function (scope, elm, attrs, ngModel) { 131 | 132 | /** 133 | * Corresponds the uiAceConfig ACE configuration. 134 | * @type object 135 | */ 136 | var options = uiAceConfig.ace || {}; 137 | 138 | /** 139 | * uiAceConfig merged with user options via json in attribute or data binding 140 | * @type object 141 | */ 142 | var opts = angular.extend({}, options, scope.$eval(attrs.uiAce)); 143 | 144 | /** 145 | * ACE editor 146 | * @type object 147 | */ 148 | var acee = window.ace.edit(elm[0]); 149 | 150 | /** 151 | * ACE editor session. 152 | * @type object 153 | * @see [EditSession]{@link http://ace.c9.io/#nav=api&api=edit_session} 154 | */ 155 | var session = acee.getSession(); 156 | 157 | /** 158 | * Reference to a change listener created by the listener factory. 159 | * @function 160 | * @see listenerFactory.onChange 161 | */ 162 | var onChangeListener; 163 | 164 | /** 165 | * Reference to a blur listener created by the listener factory. 166 | * @function 167 | * @see listenerFactory.onBlur 168 | */ 169 | var onBlurListener; 170 | 171 | /** 172 | * Calls a callback by checking its existing. The argument list 173 | * is variable and thus this function is relying on the arguments 174 | * object. 175 | * @throws {Error} If the callback isn't a function 176 | */ 177 | var executeUserCallback = function () { 178 | 179 | /** 180 | * The callback function grabbed from the array-like arguments 181 | * object. The first argument should always be the callback. 182 | * 183 | * @see [arguments]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/arguments} 184 | * @type {*} 185 | */ 186 | var callback = arguments[0]; 187 | 188 | /** 189 | * Arguments to be passed to the callback. These are taken 190 | * from the array-like arguments object. The first argument 191 | * is stripped because that should be the callback function. 192 | * 193 | * @see [arguments]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/arguments} 194 | * @type {Array} 195 | */ 196 | var args = Array.prototype.slice.call(arguments, 1); 197 | 198 | if (angular.isDefined(callback)) { 199 | scope.$evalAsync(function () { 200 | if (angular.isFunction(callback)) { 201 | callback(args); 202 | } else { 203 | throw new Error('ui-ace use a function as callback.'); 204 | } 205 | }); 206 | } 207 | }; 208 | 209 | /** 210 | * Listener factory. Until now only change listeners can be created. 211 | * @type object 212 | */ 213 | var listenerFactory = { 214 | /** 215 | * Creates a change listener which propagates the change event 216 | * and the editor session to the callback from the user option 217 | * onChange. It might be exchanged during runtime, if this 218 | * happens the old listener will be unbound. 219 | * 220 | * @param callback callback function defined in the user options 221 | * @see onChangeListener 222 | */ 223 | onChange: function (callback) { 224 | return function (e) { 225 | var newValue = session.getValue(); 226 | 227 | if (ngModel && newValue !== ngModel.$viewValue && 228 | // HACK make sure to only trigger the apply outside of the 229 | // digest loop 'cause ACE is actually using this callback 230 | // for any text transformation ! 231 | !scope.$$phase && !scope.$root.$$phase) { 232 | scope.$evalAsync(function () { 233 | ngModel.$setViewValue(newValue); 234 | }); 235 | } 236 | 237 | executeUserCallback(callback, e, acee); 238 | }; 239 | }, 240 | /** 241 | * Creates a blur listener which propagates the editor session 242 | * to the callback from the user option onBlur. It might be 243 | * exchanged during runtime, if this happens the old listener 244 | * will be unbound. 245 | * 246 | * @param callback callback function defined in the user options 247 | * @see onBlurListener 248 | */ 249 | onBlur: function (callback) { 250 | return function () { 251 | executeUserCallback(callback, acee); 252 | }; 253 | } 254 | }; 255 | 256 | attrs.$observe('readonly', function (value) { 257 | acee.setReadOnly(!!value || value === ''); 258 | }); 259 | 260 | // Value Blind 261 | if (ngModel) { 262 | ngModel.$formatters.push(function (value) { 263 | if (angular.isUndefined(value) || value === null) { 264 | return ''; 265 | } 266 | else if (angular.isObject(value) || angular.isArray(value)) { 267 | throw new Error('ui-ace cannot use an object or an array as a model'); 268 | } 269 | return value; 270 | }); 271 | 272 | ngModel.$render = function () { 273 | session.setValue(ngModel.$viewValue); 274 | }; 275 | } 276 | 277 | // Listen for option updates 278 | var updateOptions = function (current, previous) { 279 | if (current === previous) return; 280 | opts = angular.extend({}, options, scope.$eval(attrs.uiAce)); 281 | 282 | opts.callbacks = [ opts.onLoad ]; 283 | if (opts.onLoad !== options.onLoad) { 284 | // also call the global onLoad handler 285 | opts.callbacks.unshift(options.onLoad); 286 | } 287 | 288 | // EVENTS 289 | 290 | // unbind old change listener 291 | session.removeListener('change', onChangeListener); 292 | 293 | // bind new change listener 294 | onChangeListener = listenerFactory.onChange(opts.onChange); 295 | session.on('change', onChangeListener); 296 | 297 | // unbind old blur listener 298 | //session.removeListener('blur', onBlurListener); 299 | acee.removeListener('blur', onBlurListener); 300 | 301 | // bind new blur listener 302 | onBlurListener = listenerFactory.onBlur(opts.onBlur); 303 | acee.on('blur', onBlurListener); 304 | 305 | setOptions(acee, session, opts); 306 | }; 307 | 308 | scope.$watch(attrs.uiAce, updateOptions, /* deep watch */ true); 309 | 310 | // set the options here, even if we try to watch later, if this 311 | // line is missing things go wrong (and the tests will also fail) 312 | updateOptions(options); 313 | 314 | elm.on('$destroy', function () { 315 | acee.session.$stopWorker(); 316 | acee.destroy(); 317 | }); 318 | 319 | scope.$watch(function() { 320 | return [elm[0].offsetWidth, elm[0].offsetHeight]; 321 | }, function() { 322 | acee.resize(); 323 | acee.renderer.updateFull(); 324 | }, true); 325 | 326 | } 327 | }; 328 | }]); 329 | -------------------------------------------------------------------------------- /test/ace.spec.js: -------------------------------------------------------------------------------- 1 | describe('uiAce', function () { 2 | 'use strict'; 3 | 4 | var scope, $compile, 5 | uiConfig; 6 | 7 | beforeEach(function () { 8 | 9 | module('ui.ace'); 10 | 11 | inject(function (_$rootScope_, _$compile_, uiAceConfig) { 12 | scope = _$rootScope_.$new(); 13 | $compile = _$compile_; 14 | uiConfig = uiAceConfig; 15 | uiConfig.ace = { showGutter: false }; 16 | }); 17 | }); 18 | 19 | afterEach(function () { 20 | uiConfig = {}; 21 | }); 22 | 23 | describe('require', function () { 24 | var aceRequireFunction; 25 | 26 | beforeEach(function () { 27 | aceRequireFunction = window.ace.edit; 28 | window.ace.require = jasmine 29 | .createSpy('window.ace.require'); 30 | }); 31 | 32 | afterEach(function () { 33 | window.ace.require = aceRequireFunction; 34 | }); 35 | 36 | it('should not call window.ace.require if there is no "require" option', function () { 37 | $compile('
')(scope); 38 | expect(window.ace.require).not.toHaveBeenCalled(); 39 | }); 40 | 41 | it('should not call ace/config if a workerPath is not defined', function () { 42 | $compile('
')(scope); 43 | expect(window.ace.require).not.toHaveBeenCalledWith('ace/config'); 44 | }); 45 | 46 | it('should call ace/config if a workerPath is defined', function () { 47 | window.ace.require 48 | .and.returnValue({ 49 | set: function () {} 50 | }); 51 | //// 52 | $compile('
')(scope); 53 | expect(window.ace.require).toHaveBeenCalledWith('ace/config'); 54 | }); 55 | 56 | it('should call "set" if workerPath is defined', function () { 57 | var _config = jasmine.createSpyObj('config', ['set']); 58 | window.ace.require.and.returnValue(_config); 59 | //// 60 | $compile('
')(scope); 61 | expect(_config.set).toHaveBeenCalled(); 62 | }); 63 | 64 | it('should call "window.ace.require" for each option in "require"', function () { 65 | $compile('
')(scope); 66 | expect(window.ace.require).toHaveBeenCalled(); 67 | expect(window.ace.require.calls.count()).toEqual(2); 68 | }); 69 | }); 70 | 71 | describe('options', function () { 72 | var _ace, aceEditFunction; 73 | 74 | beforeEach(function () { 75 | aceEditFunction = window.ace.edit; 76 | window.ace.edit = jasmine 77 | .createSpy('window.ace.edit') 78 | .and.callFake(function () { 79 | _ace = aceEditFunction.apply(this, arguments); 80 | _ace.setOption = jasmine 81 | .createSpy('ace.setOption') 82 | .and.callThrough(); 83 | _ace.renderer.setOption = jasmine 84 | .createSpy('ace.setOption') 85 | .and.callThrough(); 86 | return _ace; 87 | }); 88 | }); 89 | 90 | afterEach(function () { 91 | window.ace.edit = aceEditFunction; 92 | }); 93 | 94 | it('should not trigger ace#setOption.', function () { 95 | $compile('
')(scope); 96 | expect(_ace.setOption.calls.count()).toEqual(0); 97 | }); 98 | 99 | it('should trigger ace#setOption with "advanced" options.', function () { 100 | $compile('
')(scope); 101 | expect(_ace.setOption.calls.count()).toEqual(1); 102 | expect(_ace.setOption).toHaveBeenCalledWith('enableSnippets', true); 103 | }); 104 | 105 | it('should trigger renderer#setOption with "rendererOptions" options.', function () { 106 | $compile('
')(scope); 107 | expect(_ace.renderer.setOption.calls.count()).toEqual(2); 108 | expect(_ace.renderer.setOption).toHaveBeenCalledWith('showGutter', false); 109 | expect(_ace.renderer.setOption).toHaveBeenCalledWith('maxLines', 42); 110 | }); 111 | }); 112 | 113 | describe('basic behavior', function () { 114 | 115 | it('should not throw an error when window.ace is defined', function () { 116 | function compile() { 117 | $compile('
')(scope); 118 | } 119 | 120 | expect(compile).not.toThrow(); 121 | }); 122 | 123 | it('should watch the uiAce attribute', function () { 124 | spyOn(scope, '$watch'); 125 | $compile('
')(scope); 126 | expect(scope.$watch).toHaveBeenCalled(); 127 | }); 128 | }); 129 | 130 | describe('instance', function () { 131 | var _ace, aceEditFunction; 132 | 133 | beforeEach(function () { 134 | aceEditFunction = window.ace.edit; 135 | window.ace.edit = jasmine 136 | .createSpy('window.ace.edit') 137 | .and.callFake(function () { 138 | _ace = aceEditFunction.apply(this, arguments); 139 | _ace.setReadOnly = jasmine.createSpy('ace.setReadOnly') 140 | .and.callThrough(); 141 | return _ace; 142 | }); 143 | }); 144 | 145 | afterEach(function () { 146 | window.ace.edit = aceEditFunction; 147 | }); 148 | 149 | it('should call ace.edit', function () { 150 | $compile('
')(scope); 151 | expect(_ace).toBeDefined(); 152 | }); 153 | 154 | describe('options', function () { 155 | describe('passed', function () { 156 | it('should show the showGutter', function () { 157 | $compile('
')(scope); 158 | expect(_ace.renderer).toBeDefined(); 159 | expect(_ace.renderer.getShowGutter()).toBeTruthy(); 160 | }); 161 | }); 162 | describe('global', function () { 163 | it('should hide the showGutter', function () { 164 | $compile('
')(scope); 165 | expect(_ace.renderer).toBeDefined(); 166 | expect(_ace.renderer.getShowGutter()).toBeFalsy(); 167 | }); 168 | }); 169 | describe('onLoad', function () { 170 | it('should call the local onLoad callback', function () { 171 | scope.aceLoaded = jasmine.createSpy('scope.aceLoaded'); 172 | $compile('
')(scope); 173 | expect(scope.aceLoaded).toHaveBeenCalled(); 174 | expect(scope.aceLoaded).toHaveBeenCalledWith(_ace); 175 | }); 176 | 177 | it('should call the global onLoad callback', function () { 178 | uiConfig.ace.onLoad = jasmine.createSpy('uiConfig.ace.onLoad'); 179 | $compile('
')(scope); 180 | expect(uiConfig.ace.onLoad).toHaveBeenCalled(); 181 | expect(uiConfig.ace.onLoad).toHaveBeenCalledWith(_ace); 182 | }); 183 | 184 | it('should call both local/global onLoad callbacks', function () { 185 | scope.aceLoaded = jasmine.createSpy('scope.aceLoaded'); 186 | uiConfig.ace.onLoad = jasmine.createSpy('uiConfig.ace.onLoad'); 187 | $compile('
')(scope); 188 | expect(uiConfig.ace.onLoad).toHaveBeenCalled(); 189 | expect(uiConfig.ace.onLoad).toHaveBeenCalledWith(_ace); 190 | expect(scope.aceLoaded).toHaveBeenCalled(); 191 | expect(scope.aceLoaded).toHaveBeenCalledWith(_ace); 192 | }); 193 | 194 | it('should detect same local/global onLoad callback', function () { 195 | uiConfig.ace.onLoad = jasmine.createSpy('uiConfig.ace.onLoad'); 196 | scope.aceLoaded = uiConfig.ace.onLoad; 197 | $compile('
')(scope); 198 | expect(uiConfig.ace.onLoad.calls.count()).toBe(1); 199 | expect(uiConfig.ace.onLoad).toHaveBeenCalledWith(_ace); 200 | }); 201 | }); 202 | }); 203 | 204 | describe('readOnly', function () { 205 | 206 | it('should read only option true', function () { 207 | $compile('
')(scope); 208 | scope.$apply(); 209 | expect(_ace.setReadOnly).toHaveBeenCalledWith(true); 210 | 211 | $compile('
')(scope); 212 | scope.$apply(); 213 | expect(_ace.setReadOnly).toHaveBeenCalledWith(true); 214 | 215 | $compile('
')(scope); 216 | scope.$apply('foo = true'); 217 | expect(_ace.setReadOnly).toHaveBeenCalledWith(true); 218 | 219 | }); 220 | 221 | it('should read only option false', function () { 222 | $compile('
')(scope); 223 | scope.$apply(); 224 | expect(_ace.setReadOnly).not.toHaveBeenCalled(); 225 | 226 | $compile('
')(scope); 227 | scope.$apply(); 228 | expect(_ace.setReadOnly).toHaveBeenCalledWith(false); 229 | 230 | $compile('
')(scope); 231 | scope.$apply(); 232 | expect(_ace.setReadOnly).toHaveBeenCalledWith(false); 233 | 234 | scope.$apply('foo = null'); 235 | expect(_ace.setReadOnly).toHaveBeenCalledWith(false); 236 | 237 | scope.$apply('foo = false'); 238 | expect(_ace.setReadOnly).toHaveBeenCalledWith(false); 239 | }); 240 | }); 241 | 242 | describe('when the model changes', function () { 243 | it('should update the IDE', function () { 244 | $compile('
')(scope); 245 | scope.$apply('foo = "bar"'); 246 | expect(_ace.getSession().getValue()).toBe(scope.foo); 247 | }); 248 | }); 249 | 250 | describe('when the IDE changes', function () { 251 | it('should update the model', function () { 252 | $compile('
')(scope); 253 | scope.$apply('foo = "bar"'); 254 | 255 | var value = 'baz'; 256 | _ace.getSession().setValue(value); 257 | scope.$apply(); 258 | 259 | expect(scope.foo).toBe(value); 260 | }); 261 | 262 | it('should update the IDE only if different', function () { 263 | scope.change = jasmine.createSpy('scope.change'); 264 | 265 | $compile('
')(scope); 266 | 267 | // change shouldn't be called initialy 268 | expect(scope.change).not.toHaveBeenCalled(); 269 | 270 | // change shouldn't be called when the value change is coming from the model. 271 | scope.$apply('foo = "bar"'); 272 | expect(scope.change).not.toHaveBeenCalled(); 273 | 274 | _ace.getSession().setValue('baz'); 275 | scope.$apply(); 276 | 277 | // ace removeText event + ace insertText event 278 | expect(scope.change.calls.count()).toBe(2); 279 | // ace removeText event 280 | expect(scope.change).toHaveBeenCalledWith(''); 281 | // ace insertText event 282 | expect(scope.change).toHaveBeenCalledWith('baz'); 283 | 284 | // 285 | expect(scope.foo).toBe('baz'); 286 | }); 287 | }); 288 | 289 | describe('when the model is undefined/null', function () { 290 | it('should update the IDE with an empty string', function () { 291 | $compile('
')(scope); 292 | scope.$apply(); 293 | expect(scope.foo).toBeUndefined(); 294 | expect(_ace.getSession().getValue()).toBe(''); 295 | scope.$apply('foo = "bar"'); 296 | expect(scope.foo).toBe('bar'); 297 | expect(_ace.getSession().getValue()).toBe('bar'); 298 | scope.$apply('foo = null'); 299 | expect(scope.foo).toBe(null); 300 | expect(_ace.getSession().getValue()).toBe(''); 301 | }); 302 | }); 303 | 304 | describe('when the callback is not a function', function () { 305 | it('should throw an error', function () { 306 | function compileWithObject() { 307 | scope.changing = {}; 308 | scope.$apply('foo = "bar"'); 309 | $compile('
')(scope); 310 | _ace.getSession().setValue('baz'); 311 | scope.$apply(); 312 | } 313 | 314 | expect(compileWithObject).toThrow(); 315 | }); 316 | }); 317 | 318 | it('should call destroy when the element is removed', function () { 319 | var element = $compile('
')(scope); 320 | _ace.destroy = jasmine.createSpy('ace.destroy') 321 | .and.callThrough(); 322 | _ace.session.$stopWorker = jasmine.createSpy('ace.session.$stopWorker') 323 | .and.callThrough(); 324 | 325 | element.remove(); 326 | scope.$apply(); 327 | 328 | expect(_ace.session.$stopWorker).toHaveBeenCalled(); 329 | expect(_ace.destroy).toHaveBeenCalled(); 330 | }); 331 | }); 332 | 333 | describe('when the model is an object or an array', function () { 334 | it('should throw an error', function () { 335 | function compileWithObject() { 336 | $compile('
')(scope); 337 | scope.foo = {}; 338 | scope.$apply(); 339 | } 340 | 341 | function compileWithArray() { 342 | $compile('
')(scope); 343 | scope.foo = []; 344 | scope.$apply(); 345 | } 346 | 347 | expect(compileWithObject).toThrow(); 348 | expect(compileWithArray).toThrow(); 349 | }); 350 | }); 351 | 352 | }); 353 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Sat Dec 14 2013 01:54:41 GMT+0100 (CET) 3 | module.exports = function(config) { 4 | 'use strict'; 5 | config.set({ 6 | 7 | // base path, that will be used to resolve files and exclude 8 | basePath: '..', 9 | 10 | 11 | // frameworks to use 12 | frameworks: ['jasmine'], 13 | 14 | 15 | // list of files / patterns to load in the browser 16 | files: [ 17 | 'bower_components/angular/angular.js', 18 | 'bower_components/angular-mocks/angular-mocks.js', 19 | 'bower_components/ace-builds/src-min-noconflict/ace.js', 20 | 'src/*.js', 21 | 'test/*.spec.js' 22 | ], 23 | 24 | 25 | // list of files to exclude 26 | exclude: [ 27 | 28 | ], 29 | 30 | 31 | // test results reporter to use 32 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' 33 | reporters: ['dots'], 34 | 35 | 36 | // web server port 37 | port: 9876, 38 | 39 | 40 | // enable / disable colors in the output (reporters and logs) 41 | colors: true, 42 | 43 | 44 | // level of logging 45 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 46 | logLevel: config.LOG_INFO, 47 | 48 | 49 | // enable / disable watching file and executing tests whenever any file changes 50 | autoWatch: false, 51 | 52 | 53 | // Start these browsers, currently available: 54 | // - Chrome 55 | // - ChromeCanary 56 | // - Firefox 57 | // - Opera (has to be installed with `npm install karma-opera-launcher`) 58 | // - Safari (only Mac; has to be installed with `npm install karma-safari-launcher`) 59 | // - PhantomJS 60 | // - IE (only Windows; has to be installed with `npm install karma-ie-launcher`) 61 | browsers: ['Chrome', 'Firefox', 'PhantomJS'], 62 | 63 | 64 | // If browser does not capture in given timeout [ms], kill it 65 | captureTimeout: 60000, 66 | 67 | 68 | // Continuous Integration mode 69 | // if true, it capture browsers, run tests and exit 70 | singleRun: false 71 | }); 72 | 73 | 74 | if(process.env.TRAVIS){ 75 | config.set({ 76 | browsers: ['TravisCI_Chrome', 'Firefox', 'PhantomJS'], 77 | customLaunchers: { 78 | TravisCI_Chrome: { 79 | base: 'Chrome', 80 | flags: ['--no-sandbox'] 81 | } 82 | } 83 | }); 84 | } 85 | }; 86 | --------------------------------------------------------------------------------