123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- '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:
- *
- * <ul>
- * <li>showGutter</li>
- * <li>useWrapMode</li>
- * <li>onLoad</li>
- * <li>theme</li>
- * <li>mode</li>
- * </ul>
- *
- * @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);
- }
- };
- }]);
|