'use strict'; /** * Binds a ACE Editor widget */ angular.module('ui.ace', []) .constant('uiAceConfig', {}) .directive('uiAce', ['uiAceConfig', function (uiAceConfig) { if (angular.isUndefined(window.ace)) { throw new Error('ui-ace need ace to work... (o rly?)'); } /** * Sets editor options such as the wrapping mode or the syntax checker. * * The supported options are: * * * * @param acee * @param session ACE editor session * @param {object} opts Options to be set */ var setOptions = function(acee, session, opts) { // sets the ace worker path, if running from concatenated // or minified source if (angular.isDefined(opts.workerPath)) { var config = window.ace.require('ace/config'); config.set('workerPath', opts.workerPath); } // ace requires loading if (angular.isDefined(opts.require)) { opts.require.forEach(function (n) { window.ace.require(n); }); } // Boolean options if (angular.isDefined(opts.showGutter)) { acee.renderer.setShowGutter(opts.showGutter); } if (angular.isDefined(opts.useWrapMode)) { session.setUseWrapMode(opts.useWrapMode); } if (angular.isDefined(opts.showInvisibles)) { acee.renderer.setShowInvisibles(opts.showInvisibles); } if (angular.isDefined(opts.showIndentGuides)) { acee.renderer.setDisplayIndentGuides(opts.showIndentGuides); } if (angular.isDefined(opts.useSoftTabs)) { session.setUseSoftTabs(opts.useSoftTabs); } if (angular.isDefined(opts.showPrintMargin)) { acee.setShowPrintMargin(opts.showPrintMargin); } // commands if (angular.isDefined(opts.disableSearch) && opts.disableSearch) { acee.commands.addCommands([ { name: 'unfind', bindKey: { win: 'Ctrl-F', mac: 'Command-F' }, exec: function () { return false; }, readOnly: true } ]); } // Basic options if (angular.isString(opts.theme)) { acee.setTheme('ace/theme/' + opts.theme); } if (angular.isString(opts.mode)) { session.setMode('ace/mode/' + opts.mode); } // Advanced options if (angular.isDefined(opts.firstLineNumber)) { if (angular.isNumber(opts.firstLineNumber)) { session.setOption('firstLineNumber', opts.firstLineNumber); } else if (angular.isFunction(opts.firstLineNumber)) { session.setOption('firstLineNumber', opts.firstLineNumber()); } } // advanced options var key, obj; if (angular.isDefined(opts.advanced)) { for (key in opts.advanced) { // create a javascript object with the key and value obj = { name: key, value: opts.advanced[key] }; // try to assign the option to the ace editor acee.setOption(obj.name, obj.value); } } // advanced options for the renderer if (angular.isDefined(opts.rendererOptions)) { for (key in opts.rendererOptions) { // create a javascript object with the key and value obj = { name: key, value: opts.rendererOptions[key] }; // try to assign the option to the ace editor acee.renderer.setOption(obj.name, obj.value); } } // onLoad callbacks angular.forEach(opts.callbacks, function (cb) { if (angular.isFunction(cb)) { cb(acee); } }); }; return { restrict: 'EA', require: '?ngModel', link: function (scope, elm, attrs, ngModel) { /** * Corresponds the uiAceConfig ACE configuration. * @type object */ var options = uiAceConfig.ace || {}; /** * uiAceConfig merged with user options via json in attribute or data binding * @type object */ var opts = angular.extend({}, options, scope.$eval(attrs.uiAce)); /** * ACE editor * @type object */ var acee = window.ace.edit(elm[0]); /** * ACE editor session. * @type object * @see [EditSession]{@link http://ace.c9.io/#nav=api&api=edit_session} */ var session = acee.getSession(); /** * Reference to a change listener created by the listener factory. * @function * @see listenerFactory.onChange */ var onChangeListener; /** * Reference to a blur listener created by the listener factory. * @function * @see listenerFactory.onBlur */ var onBlurListener; /** * Calls a callback by checking its existing. The argument list * is variable and thus this function is relying on the arguments * object. * @throws {Error} If the callback isn't a function */ var executeUserCallback = function () { /** * The callback function grabbed from the array-like arguments * object. The first argument should always be the callback. * * @see [arguments]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/arguments} * @type {*} */ var callback = arguments[0]; /** * Arguments to be passed to the callback. These are taken * from the array-like arguments object. The first argument * is stripped because that should be the callback function. * * @see [arguments]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/arguments} * @type {Array} */ var args = Array.prototype.slice.call(arguments, 1); if (angular.isDefined(callback)) { scope.$evalAsync(function () { if (angular.isFunction(callback)) { callback(args); } else { throw new Error('ui-ace use a function as callback.'); } }); } }; /** * Listener factory. Until now only change listeners can be created. * @type object */ var listenerFactory = { /** * Creates a change listener which propagates the change event * and the editor session to the callback from the user option * onChange. It might be exchanged during runtime, if this * happens the old listener will be unbound. * * @param callback callback function defined in the user options * @see onChangeListener */ onChange: function (callback) { return function (e) { var newValue = session.getValue(); if (ngModel && newValue !== ngModel.$viewValue && // HACK make sure to only trigger the apply outside of the // digest loop 'cause ACE is actually using this callback // for any text transformation ! !scope.$$phase && !scope.$root.$$phase) { scope.$evalAsync(function () { ngModel.$setViewValue(newValue); }); } executeUserCallback(callback, e, acee); }; }, /** * Creates a blur listener which propagates the editor session * to the callback from the user option onBlur. It might be * exchanged during runtime, if this happens the old listener * will be unbound. * * @param callback callback function defined in the user options * @see onBlurListener */ onBlur: function (callback) { return function () { executeUserCallback(callback, acee); }; } }; attrs.$observe('readonly', function (value) { acee.setReadOnly(!!value || value === ''); }); // Value Blind if (ngModel) { ngModel.$formatters.push(function (value) { if (angular.isUndefined(value) || value === null) { return ''; } else if (angular.isObject(value) || angular.isArray(value)) { throw new Error('ui-ace cannot use an object or an array as a model'); } return value; }); ngModel.$render = function () { session.setValue(ngModel.$viewValue); }; } // Listen for option updates var updateOptions = function (current, previous) { if (current === previous) return; opts = angular.extend({}, options, scope.$eval(attrs.uiAce)); opts.callbacks = [ opts.onLoad ]; if (opts.onLoad !== options.onLoad) { // also call the global onLoad handler opts.callbacks.unshift(options.onLoad); } // EVENTS // unbind old change listener session.removeListener('change', onChangeListener); // bind new change listener onChangeListener = listenerFactory.onChange(opts.onChange); session.on('change', onChangeListener); // unbind old blur listener //session.removeListener('blur', onBlurListener); acee.removeListener('blur', onBlurListener); // bind new blur listener onBlurListener = listenerFactory.onBlur(opts.onBlur); acee.on('blur', onBlurListener); setOptions(acee, session, opts); }; scope.$watch(attrs.uiAce, updateOptions, /* deep watch */ true); // set the options here, even if we try to watch later, if this // line is missing things go wrong (and the tests will also fail) updateOptions(options); elm.on('$destroy', function () { acee.session.$stopWorker(); acee.destroy(); }); scope.$watch(function() { return [elm[0].offsetWidth, elm[0].offsetHeight]; }, function() { acee.resize(); acee.renderer.updateFull(); }, true); } }; }]);