ui-ace.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. 'use strict';
  2. /**
  3. * Binds a ACE Editor widget
  4. */
  5. angular.module('ui.ace', [])
  6. .constant('uiAceConfig', {})
  7. .directive('uiAce', ['uiAceConfig', function (uiAceConfig) {
  8. if (angular.isUndefined(window.ace)) {
  9. throw new Error('ui-ace need ace to work... (o rly?)');
  10. }
  11. /**
  12. * Sets editor options such as the wrapping mode or the syntax checker.
  13. *
  14. * The supported options are:
  15. *
  16. * <ul>
  17. * <li>showGutter</li>
  18. * <li>useWrapMode</li>
  19. * <li>onLoad</li>
  20. * <li>theme</li>
  21. * <li>mode</li>
  22. * </ul>
  23. *
  24. * @param acee
  25. * @param session ACE editor session
  26. * @param {object} opts Options to be set
  27. */
  28. var setOptions = function(acee, session, opts) {
  29. // sets the ace worker path, if running from concatenated
  30. // or minified source
  31. if (angular.isDefined(opts.workerPath)) {
  32. var config = window.ace.require('ace/config');
  33. config.set('workerPath', opts.workerPath);
  34. }
  35. // ace requires loading
  36. if (angular.isDefined(opts.require)) {
  37. opts.require.forEach(function (n) {
  38. window.ace.require(n);
  39. });
  40. }
  41. // Boolean options
  42. if (angular.isDefined(opts.showGutter)) {
  43. acee.renderer.setShowGutter(opts.showGutter);
  44. }
  45. if (angular.isDefined(opts.useWrapMode)) {
  46. session.setUseWrapMode(opts.useWrapMode);
  47. }
  48. if (angular.isDefined(opts.showInvisibles)) {
  49. acee.renderer.setShowInvisibles(opts.showInvisibles);
  50. }
  51. if (angular.isDefined(opts.showIndentGuides)) {
  52. acee.renderer.setDisplayIndentGuides(opts.showIndentGuides);
  53. }
  54. if (angular.isDefined(opts.useSoftTabs)) {
  55. session.setUseSoftTabs(opts.useSoftTabs);
  56. }
  57. if (angular.isDefined(opts.showPrintMargin)) {
  58. acee.setShowPrintMargin(opts.showPrintMargin);
  59. }
  60. // commands
  61. if (angular.isDefined(opts.disableSearch) && opts.disableSearch) {
  62. acee.commands.addCommands([
  63. {
  64. name: 'unfind',
  65. bindKey: {
  66. win: 'Ctrl-F',
  67. mac: 'Command-F'
  68. },
  69. exec: function () {
  70. return false;
  71. },
  72. readOnly: true
  73. }
  74. ]);
  75. }
  76. // Basic options
  77. if (angular.isString(opts.theme)) {
  78. acee.setTheme('ace/theme/' + opts.theme);
  79. }
  80. if (angular.isString(opts.mode)) {
  81. session.setMode('ace/mode/' + opts.mode);
  82. }
  83. // Advanced options
  84. if (angular.isDefined(opts.firstLineNumber)) {
  85. if (angular.isNumber(opts.firstLineNumber)) {
  86. session.setOption('firstLineNumber', opts.firstLineNumber);
  87. } else if (angular.isFunction(opts.firstLineNumber)) {
  88. session.setOption('firstLineNumber', opts.firstLineNumber());
  89. }
  90. }
  91. // advanced options
  92. var key, obj;
  93. if (angular.isDefined(opts.advanced)) {
  94. for (key in opts.advanced) {
  95. // create a javascript object with the key and value
  96. obj = { name: key, value: opts.advanced[key] };
  97. // try to assign the option to the ace editor
  98. acee.setOption(obj.name, obj.value);
  99. }
  100. }
  101. // advanced options for the renderer
  102. if (angular.isDefined(opts.rendererOptions)) {
  103. for (key in opts.rendererOptions) {
  104. // create a javascript object with the key and value
  105. obj = { name: key, value: opts.rendererOptions[key] };
  106. // try to assign the option to the ace editor
  107. acee.renderer.setOption(obj.name, obj.value);
  108. }
  109. }
  110. // onLoad callbacks
  111. angular.forEach(opts.callbacks, function (cb) {
  112. if (angular.isFunction(cb)) {
  113. cb(acee);
  114. }
  115. });
  116. };
  117. return {
  118. restrict: 'EA',
  119. require: '?ngModel',
  120. link: function (scope, elm, attrs, ngModel) {
  121. /**
  122. * Corresponds the uiAceConfig ACE configuration.
  123. * @type object
  124. */
  125. var options = uiAceConfig.ace || {};
  126. /**
  127. * uiAceConfig merged with user options via json in attribute or data binding
  128. * @type object
  129. */
  130. var opts = angular.extend({}, options, scope.$eval(attrs.uiAce));
  131. /**
  132. * ACE editor
  133. * @type object
  134. */
  135. var acee = window.ace.edit(elm[0]);
  136. /**
  137. * ACE editor session.
  138. * @type object
  139. * @see [EditSession]{@link http://ace.c9.io/#nav=api&api=edit_session}
  140. */
  141. var session = acee.getSession();
  142. /**
  143. * Reference to a change listener created by the listener factory.
  144. * @function
  145. * @see listenerFactory.onChange
  146. */
  147. var onChangeListener;
  148. /**
  149. * Reference to a blur listener created by the listener factory.
  150. * @function
  151. * @see listenerFactory.onBlur
  152. */
  153. var onBlurListener;
  154. /**
  155. * Calls a callback by checking its existing. The argument list
  156. * is variable and thus this function is relying on the arguments
  157. * object.
  158. * @throws {Error} If the callback isn't a function
  159. */
  160. var executeUserCallback = function () {
  161. /**
  162. * The callback function grabbed from the array-like arguments
  163. * object. The first argument should always be the callback.
  164. *
  165. * @see [arguments]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/arguments}
  166. * @type {*}
  167. */
  168. var callback = arguments[0];
  169. /**
  170. * Arguments to be passed to the callback. These are taken
  171. * from the array-like arguments object. The first argument
  172. * is stripped because that should be the callback function.
  173. *
  174. * @see [arguments]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/arguments}
  175. * @type {Array}
  176. */
  177. var args = Array.prototype.slice.call(arguments, 1);
  178. if (angular.isDefined(callback)) {
  179. scope.$evalAsync(function () {
  180. if (angular.isFunction(callback)) {
  181. callback(args);
  182. } else {
  183. throw new Error('ui-ace use a function as callback.');
  184. }
  185. });
  186. }
  187. };
  188. /**
  189. * Listener factory. Until now only change listeners can be created.
  190. * @type object
  191. */
  192. var listenerFactory = {
  193. /**
  194. * Creates a change listener which propagates the change event
  195. * and the editor session to the callback from the user option
  196. * onChange. It might be exchanged during runtime, if this
  197. * happens the old listener will be unbound.
  198. *
  199. * @param callback callback function defined in the user options
  200. * @see onChangeListener
  201. */
  202. onChange: function (callback) {
  203. return function (e) {
  204. var newValue = session.getValue();
  205. if (ngModel && newValue !== ngModel.$viewValue &&
  206. // HACK make sure to only trigger the apply outside of the
  207. // digest loop 'cause ACE is actually using this callback
  208. // for any text transformation !
  209. !scope.$$phase && !scope.$root.$$phase) {
  210. scope.$evalAsync(function () {
  211. ngModel.$setViewValue(newValue);
  212. });
  213. }
  214. executeUserCallback(callback, e, acee);
  215. };
  216. },
  217. /**
  218. * Creates a blur listener which propagates the editor session
  219. * to the callback from the user option onBlur. It might be
  220. * exchanged during runtime, if this happens the old listener
  221. * will be unbound.
  222. *
  223. * @param callback callback function defined in the user options
  224. * @see onBlurListener
  225. */
  226. onBlur: function (callback) {
  227. return function () {
  228. executeUserCallback(callback, acee);
  229. };
  230. }
  231. };
  232. attrs.$observe('readonly', function (value) {
  233. acee.setReadOnly(!!value || value === '');
  234. });
  235. // Value Blind
  236. if (ngModel) {
  237. ngModel.$formatters.push(function (value) {
  238. if (angular.isUndefined(value) || value === null) {
  239. return '';
  240. }
  241. else if (angular.isObject(value) || angular.isArray(value)) {
  242. throw new Error('ui-ace cannot use an object or an array as a model');
  243. }
  244. return value;
  245. });
  246. ngModel.$render = function () {
  247. session.setValue(ngModel.$viewValue);
  248. };
  249. }
  250. // Listen for option updates
  251. var updateOptions = function (current, previous) {
  252. if (current === previous) return;
  253. opts = angular.extend({}, options, scope.$eval(attrs.uiAce));
  254. opts.callbacks = [ opts.onLoad ];
  255. if (opts.onLoad !== options.onLoad) {
  256. // also call the global onLoad handler
  257. opts.callbacks.unshift(options.onLoad);
  258. }
  259. // EVENTS
  260. // unbind old change listener
  261. session.removeListener('change', onChangeListener);
  262. // bind new change listener
  263. onChangeListener = listenerFactory.onChange(opts.onChange);
  264. session.on('change', onChangeListener);
  265. // unbind old blur listener
  266. //session.removeListener('blur', onBlurListener);
  267. acee.removeListener('blur', onBlurListener);
  268. // bind new blur listener
  269. onBlurListener = listenerFactory.onBlur(opts.onBlur);
  270. acee.on('blur', onBlurListener);
  271. setOptions(acee, session, opts);
  272. };
  273. scope.$watch(attrs.uiAce, updateOptions, /* deep watch */ true);
  274. // set the options here, even if we try to watch later, if this
  275. // line is missing things go wrong (and the tests will also fail)
  276. updateOptions(options);
  277. elm.on('$destroy', function () {
  278. acee.session.$stopWorker();
  279. acee.destroy();
  280. });
  281. scope.$watch(function() {
  282. return [elm[0].offsetWidth, elm[0].offsetHeight];
  283. }, function() {
  284. acee.resize();
  285. acee.renderer.updateFull();
  286. }, true);
  287. }
  288. };
  289. }]);