diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/editors/uiAce.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/editors/uiAce.directive.js new file mode 100644 index 0000000000..57401e5d5d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/editors/uiAce.directive.js @@ -0,0 +1,269 @@ +angular.module('umbraco') + .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) { + + // 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); + } + + // 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 + } + ]); + } + + // onLoad callback + if (angular.isFunction(opts.onLoad)) { + opts.onLoad(acee); + } + + // Basic options + if (angular.isString(opts.theme)) { + acee.setTheme('ace/theme/' + opts.theme); + } + if (angular.isString(opts.mode)) { + session.setMode('ace/mode/' + opts.mode); + } + }; + + 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.$apply(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 (newValue !== scope.$eval(attrs.value) && !scope.$$phase && !scope.$root.$$phase) { + if (angular.isDefined(ngModel)) { + scope.$apply(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 === 'true'); + }); + + // Value Blind + if (angular.isDefined(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); + }; + } + + // 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) + setOptions(acee, session, opts); + + // Listen for option updates + scope.$watch( attrs.uiAce, function() { + opts = angular.extend({}, options, scope.$eval(attrs.uiAce)); + + // 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); + }, /* deep watch */ true ); + + // EVENTS + onChangeListener = listenerFactory.onChange(opts.onChange); + session.on('change', onChangeListener); + + onBlurListener = listenerFactory.onBlur(opts.onBlur); + acee.on('blur', onBlurListener); + + 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); + + } + }; + }]); \ No newline at end of file diff --git a/src/Umbraco.Web/UI/JavaScript/JsInitialize.js b/src/Umbraco.Web/UI/JavaScript/JsInitialize.js index 943f10065b..b6a3a8ac1e 100644 --- a/src/Umbraco.Web/UI/JavaScript/JsInitialize.js +++ b/src/Umbraco.Web/UI/JavaScript/JsInitialize.js @@ -1,5 +1,5 @@ [ - 'lib/jquery/dist/jquery.min.js', + 'lib/jquery/jquery-2.0.3.min.js', 'lib/angular/1.1.5/angular.min.js', 'lib/underscore/underscore.js', @@ -10,7 +10,7 @@ 'lib/angular/1.1.5/angular-sanitize.min.js', 'lib/angular/angular-ui-sortable.js', - 'lib/ace-builds/src-min-noconflict/ace.js', + 'lib/ace/ace.js', 'lib/jquery/jquery.upload/js/jquery.fileupload.js', 'lib/jquery/jquery.upload/js/load-image.min.js',