diff --git a/dist/bootstrap-decorator.js b/dist/bootstrap-decorator.js deleted file mode 100644 index 09aabb8af..000000000 --- a/dist/bootstrap-decorator.js +++ /dev/null @@ -1,78 +0,0 @@ -angular.module("schemaForm").run(["$templateCache", function($templateCache) {$templateCache.put("directives/decorators/bootstrap/actions-trcl.html","
"); -$templateCache.put("directives/decorators/bootstrap/actions.html","
"); -$templateCache.put("directives/decorators/bootstrap/array.html","

{{ form.title }}

"); -$templateCache.put("directives/decorators/bootstrap/checkbox.html","
"); -$templateCache.put("directives/decorators/bootstrap/checkboxes.html","
"); -$templateCache.put("directives/decorators/bootstrap/default.html","
{{ hasSuccess() ? \'(success)\' : \'(error)\' }}
"); -$templateCache.put("directives/decorators/bootstrap/fieldset-trcl.html","
{{ form.title }}
"); -$templateCache.put("directives/decorators/bootstrap/fieldset.html","
{{ form.title }}
"); -$templateCache.put("directives/decorators/bootstrap/help.html","
"); -$templateCache.put("directives/decorators/bootstrap/radio-buttons.html","
"); -$templateCache.put("directives/decorators/bootstrap/radios-inline.html","
"); -$templateCache.put("directives/decorators/bootstrap/radios.html","
"); -$templateCache.put("directives/decorators/bootstrap/section.html","
"); -$templateCache.put("directives/decorators/bootstrap/select.html","
"); -$templateCache.put("directives/decorators/bootstrap/submit.html","
"); -$templateCache.put("directives/decorators/bootstrap/tabarray.html","
"); -$templateCache.put("directives/decorators/bootstrap/tabs.html","
"); -$templateCache.put("directives/decorators/bootstrap/textarea.html","
");}]); -angular.module('schemaForm').config(['schemaFormDecoratorsProvider', function(decoratorsProvider) { - var base = 'directives/decorators/bootstrap/'; - - decoratorsProvider.defineDecorator('bootstrapDecorator', { - textarea: {template: base + 'textarea.html', replace: false}, - fieldset: {template: base + 'fieldset.html', replace: false}, - /*fieldset: {template: base + 'fieldset.html', replace: true, builder: function(args) { - var children = args.build(args.form.items, args.path + '.items'); - console.log('fieldset children frag', children.childNodes) - args.fieldFrag.childNode.appendChild(children); - }},*/ - array: {template: base + 'array.html', replace: false}, - tabarray: {template: base + 'tabarray.html', replace: false}, - tabs: {template: base + 'tabs.html', replace: false}, - section: {template: base + 'section.html', replace: false}, - conditional: {template: base + 'section.html', replace: false}, - actions: {template: base + 'actions.html', replace: false}, - select: {template: base + 'select.html', replace: false}, - checkbox: {template: base + 'checkbox.html', replace: false}, - checkboxes: {template: base + 'checkboxes.html', replace: false}, - number: {template: base + 'default.html', replace: false}, - password: {template: base + 'default.html', replace: false}, - submit: {template: base + 'submit.html', replace: false}, - button: {template: base + 'submit.html', replace: false}, - radios: {template: base + 'radios.html', replace: false}, - 'radios-inline': {template: base + 'radios-inline.html', replace: false}, - radiobuttons: {template: base + 'radio-buttons.html', replace: false}, - help: {template: base + 'help.html', replace: false}, - 'default': {template: base + 'default.html', replace: false} - }, []); - - //manual use directives - decoratorsProvider.createDirectives({ - textarea: base + 'textarea.html', - select: base + 'select.html', - checkbox: base + 'checkbox.html', - checkboxes: base + 'checkboxes.html', - number: base + 'default.html', - submit: base + 'submit.html', - button: base + 'submit.html', - text: base + 'default.html', - date: base + 'default.html', - password: base + 'default.html', - datepicker: base + 'datepicker.html', - input: base + 'default.html', - radios: base + 'radios.html', - 'radios-inline': base + 'radios-inline.html', - radiobuttons: base + 'radio-buttons.html', - }); - -}]).directive('sfFieldset', function() { - return { - transclude: true, - scope: true, - templateUrl: 'directives/decorators/bootstrap/fieldset-trcl.html', - link: function(scope, element, attrs) { - scope.title = scope.$eval(attrs.title); - } - }; -}); diff --git a/dist/bootstrap-decorator.min.js b/dist/bootstrap-decorator.min.js deleted file mode 100644 index d0af0c0ec..000000000 --- a/dist/bootstrap-decorator.min.js +++ /dev/null @@ -1 +0,0 @@ -angular.module("schemaForm").run(["$templateCache",function(e){e.put("directives/decorators/bootstrap/actions-trcl.html",'
'),e.put("directives/decorators/bootstrap/actions.html",'
'),e.put("directives/decorators/bootstrap/array.html",'

{{ form.title }}

'),e.put("directives/decorators/bootstrap/checkbox.html",'
'),e.put("directives/decorators/bootstrap/checkboxes.html",'
'),e.put("directives/decorators/bootstrap/default.html",'
{{ hasSuccess() ? \'(success)\' : \'(error)\' }}
'),e.put("directives/decorators/bootstrap/fieldset-trcl.html",'
{{ form.title }}
'),e.put("directives/decorators/bootstrap/fieldset.html",'
{{ form.title }}
'),e.put("directives/decorators/bootstrap/help.html",'
'),e.put("directives/decorators/bootstrap/radio-buttons.html",'
'),e.put("directives/decorators/bootstrap/radios-inline.html",'
'),e.put("directives/decorators/bootstrap/radios.html",'
'),e.put("directives/decorators/bootstrap/section.html",'
'),e.put("directives/decorators/bootstrap/select.html",'
'),e.put("directives/decorators/bootstrap/submit.html",'
'),e.put("directives/decorators/bootstrap/tabarray.html",'
'),e.put("directives/decorators/bootstrap/tabs.html",'
'),e.put("directives/decorators/bootstrap/textarea.html",'
')}]),angular.module("schemaForm").config(["schemaFormDecoratorsProvider",function(e){var t="directives/decorators/bootstrap/";e.defineDecorator("bootstrapDecorator",{textarea:{template:t+"textarea.html",replace:!1},fieldset:{template:t+"fieldset.html",replace:!1},array:{template:t+"array.html",replace:!1},tabarray:{template:t+"tabarray.html",replace:!1},tabs:{template:t+"tabs.html",replace:!1},section:{template:t+"section.html",replace:!1},conditional:{template:t+"section.html",replace:!1},actions:{template:t+"actions.html",replace:!1},select:{template:t+"select.html",replace:!1},checkbox:{template:t+"checkbox.html",replace:!1},checkboxes:{template:t+"checkboxes.html",replace:!1},number:{template:t+"default.html",replace:!1},password:{template:t+"default.html",replace:!1},submit:{template:t+"submit.html",replace:!1},button:{template:t+"submit.html",replace:!1},radios:{template:t+"radios.html",replace:!1},"radios-inline":{template:t+"radios-inline.html",replace:!1},radiobuttons:{template:t+"radio-buttons.html",replace:!1},help:{template:t+"help.html",replace:!1},"default":{template:t+"default.html",replace:!1}},[]),e.createDirectives({textarea:t+"textarea.html",select:t+"select.html",checkbox:t+"checkbox.html",checkboxes:t+"checkboxes.html",number:t+"default.html",submit:t+"submit.html",button:t+"submit.html",text:t+"default.html",date:t+"default.html",password:t+"default.html",datepicker:t+"datepicker.html",input:t+"default.html",radios:t+"radios.html","radios-inline":t+"radios-inline.html",radiobuttons:t+"radio-buttons.html"})}]).directive("sfFieldset",function(){return{transclude:!0,scope:!0,templateUrl:"directives/decorators/bootstrap/fieldset-trcl.html",link:function(e,t,s){e.title=e.$eval(s.title)}}}); \ No newline at end of file diff --git a/dist/schema-form.js b/dist/schema-form.js deleted file mode 100644 index d9b93cf24..000000000 --- a/dist/schema-form.js +++ /dev/null @@ -1,2335 +0,0 @@ -(function(root, factory) { - if (typeof define === 'function' && define.amd) { - define(['angular', 'ObjectPath', 'tv4'], factory); - } else if (typeof exports === 'object') { - module.exports = factory(require('angular'), require('ObjectPath'), require('tv4')); - } else { - root.schemaForm = factory(root.angular, root.ObjectPath, root.tv4); - } -}(this, function(angular, ObjectPath, tv4) { -// Deps is sort of a problem for us, maybe in the future we will ask the user to depend -// on modules for add-ons - -var deps = []; -try { - //This throws an expection if module does not exist. - angular.module('ngSanitize'); - deps.push('ngSanitize'); -} catch (e) {} - -try { - //This throws an expection if module does not exist. - angular.module('ui.sortable'); - deps.push('ui.sortable'); -} catch (e) {} - -try { - //This throws an expection if module does not exist. - angular.module('angularSpectrumColorpicker'); - deps.push('angularSpectrumColorpicker'); -} catch (e) {} - -var schemaForm = angular.module('schemaForm', deps); - -angular.module('schemaForm').provider('sfPath', -[function() { - var sfPath = {parse: ObjectPath.parse}; - - // if we're on Angular 1.2.x, we need to continue using dot notation - if (angular.version.major === 1 && angular.version.minor < 3) { - sfPath.stringify = function(arr) { - return Array.isArray(arr) ? arr.join('.') : arr.toString(); - }; - } else { - sfPath.stringify = ObjectPath.stringify; - } - - // We want this to use whichever stringify method is defined above, - // so we have to copy the code here. - sfPath.normalize = function(data, quote) { - return sfPath.stringify(Array.isArray(data) ? data : sfPath.parse(data), quote); - }; - - // expose the methods in sfPathProvider - this.parse = sfPath.parse; - this.stringify = sfPath.stringify; - this.normalize = sfPath.normalize; - - this.$get = function() { - return sfPath; - }; -}]); - -/** - * @ngdoc service - * @name sfSelect - * @kind function - * - */ -angular.module('schemaForm').factory('sfSelect', ['sfPath', function(sfPath) { - var numRe = /^\d+$/; - - /** - * @description - * Utility method to access deep properties without - * throwing errors when things are not defined. - * Can also set a value in a deep structure, creating objects when missing - * ex. - * var foo = Select('address.contact.name',obj) - * Select('address.contact.name',obj,'Leeroy') - * - * @param {string} projection A dot path to the property you want to get/set - * @param {object} obj (optional) The object to project on, defaults to 'this' - * @param {Any} valueToSet (opional) The value to set, if parts of the path of - * the projection is missing empty objects will be created. - * @returns {Any|undefined} returns the value at the end of the projection path - * or undefined if there is none. - */ - return function(projection, obj, valueToSet) { - if (!obj) { - obj = this; - } - //Support [] array syntax - var parts = typeof projection === 'string' ? sfPath.parse(projection) : projection; - - if (typeof valueToSet !== 'undefined' && parts.length === 1) { - //special case, just setting one variable - obj[parts[0]] = valueToSet; - return obj; - } - - if (typeof valueToSet !== 'undefined' && - typeof obj[parts[0]] === 'undefined') { - // We need to look ahead to check if array is appropriate - obj[parts[0]] = parts.length > 2 && numRe.test(parts[1]) ? [] : {}; - } - - var value = obj[parts[0]]; - for (var i = 1; i < parts.length; i++) { - // Special case: We allow JSON Form syntax for arrays using empty brackets - // These will of course not work here so we exit if they are found. - if (parts[i] === '') { - return undefined; - } - if (typeof valueToSet !== 'undefined') { - if (i === parts.length - 1) { - //last step. Let's set the value - value[parts[i]] = valueToSet; - return valueToSet; - } else { - // Make sure to create new objects on the way if they are not there. - // We need to look ahead to check if array is appropriate - var tmp = value[parts[i]]; - if (typeof tmp === 'undefined' || tmp === null) { - tmp = numRe.test(parts[i + 1]) ? [] : {}; - value[parts[i]] = tmp; - } - value = tmp; - } - } else if (value) { - //Just get nex value. - value = value[parts[i]]; - } - } - return value; - }; -}]); - - -// FIXME: type template (using custom builder) -angular.module('schemaForm').factory('sfBuilder', -['$templateCache', 'schemaFormDecorators', 'sfPath', function($templateCache, schemaFormDecorators, sfPath) { - - var SNAKE_CASE_REGEXP = /[A-Z]/g; - var snakeCase = function(name, separator) { - separator = separator || '_'; - return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) { - return (pos ? separator : '') + letter.toLowerCase(); - }); - }; - - - var checkForSlot = function(form, slots) { - // Finally append this field to the frag. - // Check for slots - if (form.key) { - var slot = slots[sfPath.stringify(form.key)]; - if (slot) { - while (slot.firstChild) { - slot.removeChild(slot.firstChild); - } - return slot; - } - } - }; - - - var build = function(items, decorator, templateFn, slots, path) { - path = path || 'schemaForm.form'; - var container = document.createDocumentFragment(); - items.reduce(function(frag, f, index) { - - // Sanity check. - if (!f.type) { - return; - } - - var field = decorator[f.type] || decorator['default']; - if (!field.replace) { - // Backwards compatability build - var n = document.createElement(snakeCase(decorator.__name, '-')); - n.setAttribute('form', path + '[' + index + ']'); - (checkForSlot(f, slots) || frag).appendChild(n); - - } else { - var tmpl; - - // TODO: Create a couple fo testcases, small and large and - // measure optmization. A good start is probably a cache of DOM nodes for a particular - // template that can be cloned instead of using innerHTML - var div = document.createElement('div'); - var template = templateFn(field.template) || templateFn([decorator['default'].template]); - if (f.key) { - var key = f.key ? - sfPath.stringify(f.key).replace(/"/g, '"') : ''; - template = template.replace( - /\$\$value\$\$/g, - 'model' + (key[0] !== '[' ? '.' : '') + key - ); - } - - div.innerHTML = template; - - // Move node to a document fragment, we don't want the div. - tmpl = document.createDocumentFragment(); - while (div.childNodes.length > 0) { - tmpl.appendChild(div.childNodes[0]); - } - - - tmpl.firstChild.setAttribute('sf-field',path + '[' + index + ']'); - - // Possible builder, often a noop - field.builder({ - fieldFrag: tmpl, - form: f, - path: path + '[' + index + ']', - - // Recursive build fn - build: function(items, path) { - return build(items, decorator, templateFn, slots, path); - }, - - }); - - // Append - (checkForSlot(f, slots) || frag).appendChild(tmpl); - } - return frag; - }, container); - - return container; - }; - - -/* FIXME: make a utility function of this ordinary case -var transclusion = function() { - // We might be able to micro optimize here with some kind of setting - // or by checking the schema for the type (when we have those.) - // but a quick jsperf did 55 000 querySelectorAll per second (on my laptop), - // so I think this isn't the main performance hog. - var transclusions = tmpl.querySelectorAll('[sf-transclude]'); - - if (transclusions.length) { - // Before we do any transclusion we need clone the cache for later use, but just the first time. - if ([f.type] === tmpl) { - [f.type] = [f.type].cloneNode(true); - } - - for (var i = 0; i < transclusions.length; i++) { - var n = transclusions[i]; - - // The sf-transclude attribute is not a directive, but has the name of what we're supposed to - // traverse. FIXME: Tabs? How do we loop over something that is not a list of forms? - // maybe callback? - var sub = form[n.getAttribute('sf-transclude')]; - if (sub) { - sub = Array.isArray(sub) ? sub : [sub]; - - // Build the subform recursivly - n.appendChild( build(sub, templates, ) ); - - } - } - } - -}*/ - - - var builder = { - /** - * Builds a form from a canonical form definition - */ - build: function(form, decorator, slots) { - return build(form, decorator, function(url) { - return $templateCache.get(url) || ''; - }, slots); - - }, - internalBuild: build - }; - return builder; - -}]); - -angular.module('schemaForm').provider('schemaFormDecorators', -['$compileProvider', 'sfPathProvider', function($compileProvider, sfPathProvider) { - var defaultDecorator = ''; - var decorators = {}; - - // Map template after decorator and type. - var templateUrl = function(name, form) { - //schemaDecorator is alias for whatever is set as default - if (name === 'sfDecorator') { - name = defaultDecorator; - } - - var decorator = decorators[name]; - if (decorator[form.type]) { - return decorator[form.type].template; - } - - //try default - return decorator['default'].template; - }; - - var createDirective = function(name) { - $compileProvider.directive(name, - ['$parse', '$compile', '$http', '$templateCache', '$interpolate', '$q', 'sfErrorMessage', - 'sfPath','sfSelect', - function($parse, $compile, $http, $templateCache, $interpolate, $q, sfErrorMessage, - sfPath, sfSelect) { - - return { - restrict: 'AE', - replace: false, - transclude: false, - scope: true, - require: '?^sfSchema', - link: function(scope, element, attrs, sfSchema) { - - //The ngModelController is used in some templates and - //is needed for error messages, - scope.$on('schemaFormPropagateNgModelController', function(event, ngModel) { - event.stopPropagation(); - event.preventDefault(); - scope.ngModel = ngModel; - }); - - //Keep error prone logic from the template - scope.showTitle = function() { - return scope.form && scope.form.notitle !== true && scope.form.title; - }; - - scope.listToCheckboxValues = function(list) { - var values = {}; - angular.forEach(list, function(v) { - values[v] = true; - }); - return values; - }; - - scope.checkboxValuesToList = function(values) { - var lst = []; - angular.forEach(values, function(v, k) { - if (v) { - lst.push(k); - } - }); - return lst; - }; - - scope.buttonClick = function($event, form) { - if (angular.isFunction(form.onClick)) { - form.onClick($event, form); - } else if (angular.isString(form.onClick)) { - if (sfSchema) { - //evaluating in scope outside of sfSchemas isolated scope - sfSchema.evalInParentScope(form.onClick, {'$event': $event, form: form}); - } else { - scope.$eval(form.onClick, {'$event': $event, form: form}); - } - } - }; - - /** - * Evaluate an expression, i.e. scope.$eval - * but do it in sfSchemas parent scope sf-schema directive is used - * @param {string} expression - * @param {Object} locals (optional) - * @return {Any} the result of the expression - */ - scope.evalExpr = function(expression, locals) { - if (sfSchema) { - //evaluating in scope outside of sfSchemas isolated scope - return sfSchema.evalInParentScope(expression, locals); - } - - return scope.$eval(expression, locals); - }; - - /** - * Evaluate an expression, i.e. scope.$eval - * in this decorators scope - * @param {string} expression - * @param {Object} locals (optional) - * @return {Any} the result of the expression - */ - scope.evalInScope = function(expression, locals) { - if (expression) { - return scope.$eval(expression, locals); - } - }; - - /** - * Interpolate the expression. - * Similar to `evalExpr()` and `evalInScope()` - * but will not fail if the expression is - * text that contains spaces. - * - * Use the Angular `{{ interpolation }}` - * braces to access properties on `locals`. - * - * @param {string} content The string to interpolate. - * @param {Object} locals (optional) Properties that may be accessed in the - * `expression` string. - * @return {Any} The result of the expression or `undefined`. - */ - scope.interp = function(expression, locals) { - return (expression && $interpolate(expression)(locals)); - }; - - //This works since we ot the ngModel from the array or the schema-validate directive. - scope.hasSuccess = function() { - if (!scope.ngModel) { - return false; - } - return scope.ngModel.$valid && - (!scope.ngModel.$pristine || !scope.ngModel.$isEmpty(scope.ngModel.$modelValue)); - }; - - scope.hasError = function() { - if (!scope.ngModel) { - return false; - } - return scope.ngModel.$invalid && !scope.ngModel.$pristine; - }; - - /** - * DEPRECATED: use sf-messages instead. - * Error message handler - * An error can either be a schema validation message or a angular js validtion - * error (i.e. required) - */ - scope.errorMessage = function(schemaError) { - return sfErrorMessage.interpolate( - (schemaError && schemaError.code + '') || 'default', - (scope.ngModel && scope.ngModel.$modelValue) || '', - (scope.ngModel && scope.ngModel.$viewValue) || '', - scope.form, - scope.options && scope.options.validationMessage - ); - }; - - // Rebind our part of the form to the scope. - var once = scope.$watch(attrs.form, function(form) { - if (form) { - // Workaround for 'updateOn' error from ngModelOptions - // see https://github.com/Textalk/angular-schema-form/issues/255 - // and https://github.com/Textalk/angular-schema-form/issues/206 - form.ngModelOptions = form.ngModelOptions || {}; - scope.form = form; - - //ok let's replace that template! - //We do this manually since we need to bind ng-model properly and also - //for fieldsets to recurse properly. - var templatePromise; - - // type: "template" is a special case. It can contain a template inline or an url. - // otherwise we find out the url to the template and load them. - if (form.type === 'template' && form.template) { - templatePromise = $q.when(form.template); - } else { - var url = form.type === 'template' ? form.templateUrl : templateUrl(name, form); - templatePromise = $http.get(url, {cache: $templateCache}).then(function(res) { - return res.data; - }); - } - - templatePromise.then(function(template) { - if (form.key) { - var key = form.key ? - sfPathProvider.stringify(form.key).replace(/"/g, '"') : ''; - template = template.replace( - /\$\$value\$\$/g, - 'model' + (key[0] !== '[' ? '.' : '') + key - ); - } - element.html(template); - - // Do we have a condition? Then we slap on an ng-if on all children, - // but be nice to existing ng-if. - if (form.condition) { - - var evalExpr = 'evalExpr(form.condition,{ model: model, "arrayIndex": arrayIndex})'; - if (form.key) { - evalExpr = 'evalExpr(form.condition,{ model: model, "arrayIndex": arrayIndex, "modelValue": model' + sfPath.stringify(form.key) + '})'; - } - - angular.forEach(element.children(), function(child) { - var ngIf = child.getAttribute('ng-if'); - child.setAttribute( - 'ng-if', - ngIf ? - '(' + ngIf + - ') || (' + evalExpr +')' - : evalExpr - ); - }); - } - $compile(element.contents())(scope); - }); - - // Where there is a key there is probably a ngModel - if (form.key) { - // It looks better with dot notation. - scope.$on( - 'schemaForm.error.' + form.key.join('.'), - function(event, error, validationMessage, validity) { - if (validationMessage === true || validationMessage === false) { - validity = validationMessage; - validationMessage = undefined; - } - - if (scope.ngModel && error) { - if (scope.ngModel.$setDirty) { - scope.ngModel.$setDirty(); - } else { - // FIXME: Check that this actually works on 1.2 - scope.ngModel.$dirty = true; - scope.ngModel.$pristine = false; - } - - // Set the new validation message if one is supplied - // Does not work when validationMessage is just a string. - if (validationMessage) { - if (!form.validationMessage) { - form.validationMessage = {}; - } - form.validationMessage[error] = validationMessage; - } - - scope.ngModel.$setValidity(error, validity === true); - - if (validity === true) { - // Setting or removing a validity can change the field to believe its valid - // but its not. So lets trigger its validation as well. - scope.$broadcast('schemaFormValidate'); - } - } - }); - - // Clean up the model when the corresponding form field is $destroy-ed. - // Default behavior can be supplied as a globalOption, and behavior can be overridden in the form definition. - scope.$on('$destroy', function() { - // If the entire schema form is destroyed we don't touch the model - if (!scope.externalDestructionInProgress) { - var destroyStrategy = form.destroyStrategy || - (scope.options && scope.options.destroyStrategy) || 'remove'; - // No key no model, and we might have strategy 'retain' - if (form.key && destroyStrategy !== 'retain') { - - // Get the object that has the property we wan't to clear. - var obj = scope.model; - if (form.key.length > 1) { - obj = sfSelect(form.key.slice(0, form.key.length - 1), obj); - } - - // We can get undefined here if the form hasn't been filled out entirely - if (obj === undefined) { - return; - } - - // Type can also be a list in JSON Schema - var type = (form.schema && form.schema.type) || ''; - - // Empty means '',{} and [] for appropriate types and undefined for the rest - if (destroyStrategy === 'empty' && type.indexOf('string') !== -1) { - obj[form.key.slice(-1)] = ''; - } else if (destroyStrategy === 'empty' && type.indexOf('object') !== -1) { - obj[form.key.slice(-1)] = {}; - } else if (destroyStrategy === 'empty' && type.indexOf('array') !== -1) { - obj[form.key.slice(-1)] = []; - } else if (destroyStrategy === 'null') { - obj[form.key.slice(-1)] = null; - } else { - delete obj[form.key.slice(-1)]; - } - } - } - }); - } - - once(); - } - }); - } - }; - } - ]); - }; - - var createManualDirective = function(type, templateUrl, transclude) { - transclude = angular.isDefined(transclude) ? transclude : false; - $compileProvider.directive('sf' + angular.uppercase(type[0]) + type.substr(1), function() { - return { - restrict: 'EAC', - scope: true, - replace: true, - transclude: transclude, - template: '', - link: function(scope, element, attrs) { - var watchThis = { - 'items': 'c', - 'titleMap': 'c', - 'schema': 'c' - }; - var form = {type: type}; - var once = true; - angular.forEach(attrs, function(value, name) { - if (name[0] !== '$' && name.indexOf('ng') !== 0 && name !== 'sfField') { - - var updateForm = function(val) { - if (angular.isDefined(val) && val !== form[name]) { - form[name] = val; - - //when we have type, and if specified key we apply it on scope. - if (once && form.type && (form.key || angular.isUndefined(attrs.key))) { - scope.form = form; - once = false; - } - } - }; - - if (name === 'model') { - //"model" is bound to scope under the name "model" since this is what the decorators - //know and love. - scope.$watch(value, function(val) { - if (val && scope.model !== val) { - scope.model = val; - } - }); - } else if (watchThis[name] === 'c') { - //watch collection - scope.$watchCollection(value, updateForm); - } else { - //$observe - attrs.$observe(name, updateForm); - } - } - }); - } - }; - }); - }; - - /** - * DEPRECATED: use defineDecorator instead. - * Create a decorator directive and its sibling "manual" use decorators. - * The directive can be used to create form fields or other form entities. - * It can be used in conjunction with directive in which case the decorator is - * given it's configuration via a the "form" attribute. - * - * ex. Basic usage - * - ** - * @param {string} name directive name (CamelCased) - * @param {Object} templates, an object that maps "type" => "templateUrl" - */ - this.createDecorator = function(name, templates) { - console.warn('schemaFormDecorators.createDecorator is DEPRECATED, use defineDecorator instead.'); - decorators[name] = {'__name': name}; - - angular.forEach(templates, function(url, type) { - decorators[name][type] = {template: url, replace: false, builder: angular.noop}; - }); - - if (!decorators[defaultDecorator]) { - defaultDecorator = name; - } - createDirective(name); - }; - - - /** - * Create a decorator directive and its sibling "manual" use decorators. - * The directive can be used to create form fields or other form entities. - * It can be used in conjunction with directive in which case the decorator is - * given it's configuration via a the "form" attribute. - * - * ex. Basic usage - * - ** - * @param {string} name directive name (CamelCased) - * @param {Object} fields, an object that maps "type" => `{ template, builder, replace}`. - attributes `builder` and `replace` are optional, and replace defaults to true. - */ - this.defineDecorator = function(name, fields) { - decorators[name] = {'__name': name}; // TODO: this feels like a hack, come up with a better way. - - angular.forEach(fields, function(field, type) { - field.builder = field.builder || angular.noop; - field.replace = angular.isDefined(field.replace) ? field.replace : true; - decorators[name][type] = field; - }); - - if (!decorators[defaultDecorator]) { - defaultDecorator = name; - } - createDirective(name); - }; - - /** - * Creates a directive of a decorator - * Usable when you want to use the decorators without using directive. - * Specifically when you need to reuse styling. - * - * ex. createDirective('text','...') - * - * - * @param {string} type The type of the directive, resulting directive will have sf- prefixed - * @param {string} templateUrl - * @param {boolean} transclude (optional) sets transclude option of directive, defaults to false. - */ - this.createDirective = createManualDirective; - - /** - * Same as createDirective, but takes an object where key is 'type' and value is 'templateUrl' - * Useful for batching. - * @param {Object} templates - */ - this.createDirectives = function(templates) { - angular.forEach(templates, function(url, type) { - createManualDirective(type, url); - }); - }; - - /** - * Getter for decorator settings - * @param {string} name (optional) defaults to defaultDecorator - * @return {Object} rules and templates { rules: [],templates: {}} - */ - this.decorator = function(name) { - name = name || defaultDecorator; - return decorators[name]; - }; - - - /** - * Adds a mapping to an existing decorator. - * @param {String} name Decorator name - * @param {String} type Form type for the mapping - * @param {String} url The template url - * @param {Function} builder (optional) builder function - * @param {boolean} replace (optional) defaults to false. Replace decorator directive with template. - */ - this.addMapping = function(name, type, url, builder, replace) { - if (decorators[name]) { - decorators[name][type] = { - template: url, - builder: builder, - replace: !!replace - }; - } - }; - - //Service is just a getter for directive templates and rules - this.$get = function() { - return { - decorator: function(name) { - return decorators[name] || decorators[defaultDecorator]; - }, - defaultDecorator: defaultDecorator - }; - }; - - //Create a default directive - createDirective('sfDecorator'); - -}]); - -angular.module('schemaForm').provider('sfErrorMessage', function() { - - // The codes are tv4 error codes. - // Not all of these can actually happen in a field, but for - // we never know when one might pop up so it's best to cover them all. - - // TODO: Humanize these. - var defaultMessages = { - 'default': 'Field does not validate', - 0: 'Invalid type, expected {{schema.type}}', - 1: 'No enum match for: {{value}}', - 10: 'Data does not match any schemas from "anyOf"', - 11: 'Data does not match any schemas from "oneOf"', - 12: 'Data is valid against more than one schema from "oneOf"', - 13: 'Data matches schema from "not"', - // Numeric errors - 100: 'Value is not a multiple of {{schema.divisibleBy}}', - 101: '{{viewValue}} is less than the allowed minimum of {{schema.minimum}}', - 102: '{{viewValue}} is equal to the exclusive minimum {{schema.minimum}}', - 103: '{{viewValue}} is greater than the allowed maximum of {{schema.maximum}}', - 104: '{{viewValue}} is equal to the exclusive maximum {{schema.maximum}}', - 105: 'Value is not a valid number', - // String errors - 200: 'String is too short ({{viewValue.length}} chars), minimum {{schema.minLength}}', - 201: 'String is too long ({{viewValue.length}} chars), maximum {{schema.maxLength}}', - 202: 'String does not match pattern: {{schema.pattern}}', - // Object errors - 300: 'Too few properties defined, minimum {{schema.minProperties}}', - 301: 'Too many properties defined, maximum {{schema.maxProperties}}', - 302: 'Required', - 303: 'Additional properties not allowed', - 304: 'Dependency failed - key must exist', - // Array errors - 400: 'Array is too short ({{value.length}}), minimum {{schema.maxItems}}', - 401: 'Array is too long ({{value.length}}), maximum {{schema.minItems}}', - 402: 'Array items are not unique', - 403: 'Additional items not allowed', - // Format errors - 500: 'Format validation failed', - 501: 'Keyword failed: "{{title}}"', - // Schema structure - 600: 'Circular $refs', - // Non-standard validation options - 1000: 'Unknown property (not in schema)' - }; - - // In some cases we get hit with an angular validation error - defaultMessages.number = defaultMessages[105]; - defaultMessages.required = defaultMessages[302]; - defaultMessages.min = defaultMessages[101]; - defaultMessages.max = defaultMessages[103]; - defaultMessages.maxlength = defaultMessages[201]; - defaultMessages.minlength = defaultMessages[200]; - defaultMessages.pattern = defaultMessages[202]; - - this.setDefaultMessages = function(messages) { - defaultMessages = messages; - }; - - this.getDefaultMessages = function() { - return defaultMessages; - }; - - this.setDefaultMessage = function(error, msg) { - defaultMessages[error] = msg; - }; - - this.$get = ['$interpolate', function($interpolate) { - - var service = {}; - service.defaultMessages = defaultMessages; - - /** - * Interpolate and return proper error for an eror code. - * Validation message on form trumps global error messages. - * and if the message is a function instead of a string that function will be called instead. - * @param {string} error the error code, i.e. tv4-xxx for tv4 errors, otherwise it's whats on - * ngModel.$error for custom errors. - * @param {Any} value the actual model value. - * @param {Any} viewValue the viewValue - * @param {Object} form a form definition object for this field - * @param {Object} global the global validation messages object (even though its called global - * its actually just shared in one instance of sf-schema) - * @return {string} The error message. - */ - service.interpolate = function(error, value, viewValue, form, global) { - global = global || {}; - var validationMessage = form.validationMessage || {}; - - // Drop tv4 prefix so only the code is left. - if (error.indexOf('tv4-') === 0) { - error = error.substring(4); - } - - // First find apropriate message or function - var message = validationMessage['default'] || global['default'] || ''; - - [validationMessage, global, defaultMessages].some(function(val) { - if (angular.isString(val) || angular.isFunction(val)) { - message = val; - return true; - } - if (val && val[error]) { - message = val[error]; - return true; - } - }); - - var context = { - error: error, - value: value, - viewValue: viewValue, - form: form, - schema: form.schema, - title: form.title || (form.schema && form.schema.title) - }; - if (angular.isFunction(message)) { - return message(context); - } else { - return $interpolate(message)(context); - } - }; - - return service; - }]; - -}); - -/** - * Schema form service. - * This service is not that useful outside of schema form directive - * but makes the code more testable. - */ -angular.module('schemaForm').provider('schemaForm', -['sfPathProvider', function(sfPathProvider) { - var stripNullType = function(type) { - if (Array.isArray(type) && type.length == 2) { - if (type[0] === 'null') - return type[1]; - if (type[1] === 'null') - return type[0]; - } - return type; - }; - - //Creates an default titleMap list from an enum, i.e. a list of strings. - var enumToTitleMap = function(enm) { - var titleMap = []; //canonical titleMap format is a list. - enm.forEach(function(name) { - titleMap.push({name: name, value: name}); - }); - return titleMap; - }; - - // Takes a titleMap in either object or list format and returns one in - // in the list format. - var canonicalTitleMap = function(titleMap, originalEnum) { - if (!angular.isArray(titleMap)) { - var canonical = []; - if (originalEnum) { - angular.forEach(originalEnum, function(value, index) { - canonical.push({name: titleMap[value], value: value}); - }); - } else { - angular.forEach(titleMap, function(name, value) { - canonical.push({name: name, value: value}); - }); - } - return canonical; - } - return titleMap; - }; - - var defaultFormDefinition = function(name, schema, options) { - var rules = defaults[stripNullType(schema.type)]; - if (rules) { - var def; - for (var i = 0; i < rules.length; i++) { - def = rules[i](name, schema, options); - - //first handler in list that actually returns something is our handler! - if (def) { - - // Do we have form defaults in the schema under the x-schema-form-attribute? - if (def.schema['x-schema-form'] && angular.isObject(def.schema['x-schema-form'])) { - def = angular.extend(def, def.schema['x-schema-form']); - } - - return def; - } - } - } - }; - - //Creates a form object with all common properties - var stdFormObj = function(name, schema, options) { - options = options || {}; - var f = options.global && options.global.formDefaults ? - angular.copy(options.global.formDefaults) : {}; - if (options.global && options.global.supressPropertyTitles === true) { - f.title = schema.title; - } else { - f.title = schema.title || name; - } - - if (schema.description) { f.description = schema.description; } - if (options.required === true || schema.required === true) { f.required = true; } - if (schema.maxLength) { f.maxlength = schema.maxLength; } - if (schema.minLength) { f.minlength = schema.maxLength; } - if (schema.readOnly || schema.readonly) { f.readonly = true; } - if (schema.minimum) { f.minimum = schema.minimum + (schema.exclusiveMinimum ? 1 : 0); } - if (schema.maximum) { f.maximum = schema.maximum - (schema.exclusiveMaximum ? 1 : 0); } - - // Non standard attributes (DONT USE DEPRECATED) - // If you must set stuff like this in the schema use the x-schema-form attribute - if (schema.validationMessage) { f.validationMessage = schema.validationMessage; } - if (schema.enumNames) { f.titleMap = canonicalTitleMap(schema.enumNames, schema['enum']); } - f.schema = schema; - - // Ng model options doesn't play nice with undefined, might be defined - // globally though - f.ngModelOptions = f.ngModelOptions || {}; - - return f; - }; - - var text = function(name, schema, options) { - if (stripNullType(schema.type) === 'string' && !schema['enum']) { - var f = stdFormObj(name, schema, options); - f.key = options.path; - f.type = 'text'; - options.lookup[sfPathProvider.stringify(options.path)] = f; - return f; - } - }; - - //default in json form for number and integer is a text field - //input type="number" would be more suitable don't ya think? - var number = function(name, schema, options) { - if (stripNullType(schema.type) === 'number') { - var f = stdFormObj(name, schema, options); - f.key = options.path; - f.type = 'number'; - options.lookup[sfPathProvider.stringify(options.path)] = f; - return f; - } - }; - - var integer = function(name, schema, options) { - if (stripNullType(schema.type) === 'integer') { - var f = stdFormObj(name, schema, options); - f.key = options.path; - f.type = 'number'; - options.lookup[sfPathProvider.stringify(options.path)] = f; - return f; - } - }; - - var checkbox = function(name, schema, options) { - if (stripNullType(schema.type) === 'boolean') { - var f = stdFormObj(name, schema, options); - f.key = options.path; - f.type = 'checkbox'; - options.lookup[sfPathProvider.stringify(options.path)] = f; - return f; - } - }; - - var select = function(name, schema, options) { - if (stripNullType(schema.type) === 'string' && schema['enum']) { - var f = stdFormObj(name, schema, options); - f.key = options.path; - f.type = 'select'; - if (!f.titleMap) { - f.titleMap = enumToTitleMap(schema['enum']); - } - f.trackBy = 'value'; - options.lookup[sfPathProvider.stringify(options.path)] = f; - return f; - } - }; - - var checkboxes = function(name, schema, options) { - if (stripNullType(schema.type) === 'array' && schema.items && schema.items['enum']) { - var f = stdFormObj(name, schema, options); - f.key = options.path; - f.type = 'checkboxes'; - if (!f.titleMap) { - f.titleMap = enumToTitleMap(schema.items['enum']); - } - options.lookup[sfPathProvider.stringify(options.path)] = f; - return f; - } - }; - - var fieldset = function(name, schema, options) { - if (stripNullType(schema.type) === 'object') { - var f = stdFormObj(name, schema, options); - f.type = 'fieldset'; - f.items = []; - options.lookup[sfPathProvider.stringify(options.path)] = f; - - //recurse down into properties - angular.forEach(schema.properties, function(v, k) { - var path = options.path.slice(); - path.push(k); - if (options.ignore[sfPathProvider.stringify(path)] !== true) { - var required = schema.required && schema.required.indexOf(k) !== -1; - - var def = defaultFormDefinition(k, v, { - path: path, - required: required || false, - lookup: options.lookup, - ignore: options.ignore, - global: options.global - }); - if (def) { - f.items.push(def); - } - } - }); - - return f; - } - - }; - - var array = function(name, schema, options) { - - if (stripNullType(schema.type) === 'array') { - var f = stdFormObj(name, schema, options); - f.type = 'array'; - f.key = options.path; - options.lookup[sfPathProvider.stringify(options.path)] = f; - - var required = schema.required && - schema.required.indexOf(options.path[options.path.length - 1]) !== -1; - - // The default is to always just create one child. This works since if the - // schemas items declaration is of type: "object" then we get a fieldset. - // We also follow json form notatation, adding empty brackets "[]" to - // signify arrays. - - var arrPath = options.path.slice(); - arrPath.push(''); - - f.items = [defaultFormDefinition(name, schema.items, { - path: arrPath, - required: required || false, - lookup: options.lookup, - ignore: options.ignore, - global: options.global - })]; - - return f; - } - - }; - - //First sorted by schema type then a list. - //Order has importance. First handler returning an form snippet will be used. - var defaults = { - string: [select, text], - object: [fieldset], - number: [number], - integer: [integer], - boolean: [checkbox], - array: [checkboxes, array] - }; - - var postProcessFn = function(form) { return form; }; - - /** - * Provider API - */ - this.defaults = defaults; - this.stdFormObj = stdFormObj; - this.defaultFormDefinition = defaultFormDefinition; - - /** - * Register a post process function. - * This function is called with the fully merged - * form definition (i.e. after merging with schema) - * and whatever it returns is used as form. - */ - this.postProcess = function(fn) { - postProcessFn = fn; - }; - - /** - * Append default form rule - * @param {string} type json schema type - * @param {Function} rule a function(propertyName,propertySchema,options) that returns a form - * definition or undefined - */ - this.appendRule = function(type, rule) { - if (!defaults[type]) { - defaults[type] = []; - } - defaults[type].push(rule); - }; - - /** - * Prepend default form rule - * @param {string} type json schema type - * @param {Function} rule a function(propertyName,propertySchema,options) that returns a form - * definition or undefined - */ - this.prependRule = function(type, rule) { - if (!defaults[type]) { - defaults[type] = []; - } - defaults[type].unshift(rule); - }; - - /** - * Utility function to create a standard form object. - * This does *not* set the type of the form but rather all shared attributes. - * You probably want to start your rule with creating the form with this method - * then setting type and any other values you need. - * @param {Object} schema - * @param {Object} options - * @return {Object} a form field defintion - */ - this.createStandardForm = stdFormObj; - /* End Provider API */ - - this.$get = function() { - - var service = {}; - - service.merge = function(schema, form, ignore, options, readonly) { - form = form || ['*']; - options = options || {}; - - // Get readonly from root object - readonly = readonly || schema.readonly || schema.readOnly; - - var stdForm = service.defaults(schema, ignore, options); - - //simple case, we have a "*", just put the stdForm there - var idx = form.indexOf('*'); - if (idx !== -1) { - form = form.slice(0, idx) - .concat(stdForm.form) - .concat(form.slice(idx + 1)); - } - - //ok let's merge! - //We look at the supplied form and extend it with schema standards - var lookup = stdForm.lookup; - - return postProcessFn(form.map(function(obj) { - - //handle the shortcut with just a name - if (typeof obj === 'string') { - obj = {key: obj}; - } - - if (obj.key) { - if (typeof obj.key === 'string') { - obj.key = sfPathProvider.parse(obj.key); - } - } - - //If it has a titleMap make sure it's a list - if (obj.titleMap) { - obj.titleMap = canonicalTitleMap(obj.titleMap); - } - - if(obj.type === 'select') { - obj.trackBy = obj.trackBy || 'value'; - } - - // - if (obj.itemForm) { - obj.items = []; - var str = sfPathProvider.stringify(obj.key); - var stdForm = lookup[str]; - angular.forEach(stdForm.items, function(item) { - var o = angular.copy(obj.itemForm); - o.key = item.key; - obj.items.push(o); - }); - } - - //extend with std form from schema. - if (obj.key) { - var strid = sfPathProvider.stringify(obj.key); - if (lookup[strid]) { - var schemaDefaults = lookup[strid]; - angular.forEach(schemaDefaults, function(value, attr) { - if (obj[attr] === undefined) { - obj[attr] = schemaDefaults[attr]; - } - }); - } - } - - // Are we inheriting readonly? - if (readonly === true) { // Inheriting false is not cool. - obj.readonly = true; - } - - //if it's a type with items, merge 'em! - if (obj.items) { - obj.items = service.merge(schema, obj.items, ignore, options, obj.readonly); - } - - //if its has tabs, merge them also! - if (obj.tabs) { - angular.forEach(obj.tabs, function(tab) { - tab.items = service.merge(schema, tab.items, ignore, options, obj.readonly); - }); - } - - // Special case: checkbox - // Since have to ternary state we need a default - if (obj.type === 'checkbox' && angular.isUndefined(obj.schema['default'])) { - obj.schema['default'] = false; - } - - return obj; - })); - }; - - /** - * Create form defaults from schema - */ - service.defaults = function(schema, ignore, globalOptions) { - var form = []; - var lookup = {}; //Map path => form obj for fast lookup in merging - ignore = ignore || {}; - globalOptions = globalOptions || {}; - - if (stripNullType(schema.type) === 'object') { - angular.forEach(schema.properties, function(v, k) { - if (ignore[k] !== true) { - var required = schema.required && schema.required.indexOf(k) !== -1; - var def = defaultFormDefinition(k, v, { - path: [k], // Path to this property in bracket notation. - lookup: lookup, // Extra map to register with. Optimization for merger. - ignore: ignore, // The ignore list of paths (sans root level name) - required: required, // Is it required? (v4 json schema style) - global: globalOptions // Global options, including form defaults - }); - if (def) { - form.push(def); - } - } - }); - - } else { - throw new Error('Not implemented. Only type "object" allowed at root level of schema.'); - } - return {form: form, lookup: lookup}; - }; - - //Utility functions - /** - * Traverse a schema, applying a function(schema,path) on every sub schema - * i.e. every property of an object. - */ - service.traverseSchema = function(schema, fn, path, ignoreArrays) { - ignoreArrays = angular.isDefined(ignoreArrays) ? ignoreArrays : true; - - path = path || []; - - var traverse = function(schema, fn, path) { - fn(schema, path); - angular.forEach(schema.properties, function(prop, name) { - var currentPath = path.slice(); - currentPath.push(name); - traverse(prop, fn, currentPath); - }); - - //Only support type "array" which have a schema as "items". - if (!ignoreArrays && schema.items) { - var arrPath = path.slice(); arrPath.push(''); - traverse(schema.items, fn, arrPath); - } - }; - - traverse(schema, fn, path || []); - }; - - service.traverseForm = function(form, fn) { - fn(form); - angular.forEach(form.items, function(f) { - service.traverseForm(f, fn); - }); - - if (form.tabs) { - angular.forEach(form.tabs, function(tab) { - angular.forEach(tab.items, function(f) { - service.traverseForm(f, fn); - }); - }); - } - }; - - return service; - }; - -}]); - -/* Common code for validating a value against its form and schema definition */ -/* global tv4 */ -angular.module('schemaForm').factory('sfValidator', [function() { - - var validator = {}; - - /** - * Validate a value against its form definition and schema. - * The value should either be of proper type or a string, some type - * coercion is applied. - * - * @param {Object} form A merged form definition, i.e. one with a schema. - * @param {Any} value the value to validate. - * @return a tv4js result object. - */ - validator.validate = function(form, value) { - if (!form) { - return {valid: true}; - } - var schema = form.schema; - - if (!schema) { - return {valid: true}; - } - - // Input of type text and textareas will give us a viewValue of '' - // when empty, this is a valid value in a schema and does not count as something - // that breaks validation of 'required'. But for our own sanity an empty field should - // not validate if it's required. - if (value === '') { - value = undefined; - } - - // Numbers fields will give a null value, which also means empty field - if (form.type === 'number' && value === null) { - value = undefined; - } - - // Version 4 of JSON Schema has the required property not on the - // property itself but on the wrapping object. Since we like to test - // only this property we wrap it in a fake object. - var wrap = {type: 'object', 'properties': {}}; - var propName = form.key[form.key.length - 1]; - wrap.properties[propName] = schema; - - if (form.required) { - wrap.required = [propName]; - } - var valueWrap = {}; - if (angular.isDefined(value)) { - valueWrap[propName] = value; - } - return tv4.validateResult(valueWrap, wrap); - - }; - - return validator; -}]); - -/** - * Directive that handles the model arrays - */ -angular.module('schemaForm').directive('sfArray', ['sfSelect', 'schemaForm', 'sfValidator', 'sfPath', - function(sfSelect, schemaForm, sfValidator, sfPath) { - - var setIndex = function(index) { - return function(form) { - if (form.key) { - form.key[form.key.indexOf('')] = index; - } - }; - }; - - return { - restrict: 'A', - scope: true, - require: '?ngModel', - link: function(scope, element, attrs, ngModel) { - var formDefCache = {}; - - scope.validateArray = angular.noop; - - if (ngModel) { - // We need the ngModelController on several places, - // most notably for errors. - // So we emit it up to the decorator directive so it can put it on scope. - scope.$emit('schemaFormPropagateNgModelController', ngModel); - } - - - // Watch for the form definition and then rewrite it. - // It's the (first) array part of the key, '[]' that needs a number - // corresponding to an index of the form. - var once = scope.$watch(attrs.sfArray, function(form) { - - // An array model always needs a key so we know what part of the model - // to look at. This makes us a bit incompatible with JSON Form, on the - // other hand it enables two way binding. - var list = sfSelect(form.key, scope.model); - - // We only modify the same array instance but someone might change the array from - // the outside so let's watch for that. We use an ordinary watch since the only case - // we're really interested in is if its a new instance. - scope.$watch('model' + sfPath.normalize(form.key), function(value) { - list = scope.modelArray = value; - }); - - // Since ng-model happily creates objects in a deep path when setting a - // a value but not arrays we need to create the array. - if (angular.isUndefined(list)) { - list = []; - sfSelect(form.key, scope.model, list); - } - scope.modelArray = list; - - // Arrays with titleMaps, i.e. checkboxes doesn't have items. - if (form.items) { - - // To be more compatible with JSON Form we support an array of items - // in the form definition of "array" (the schema just a value). - // for the subforms code to work this means we wrap everything in a - // section. Unless there is just one. - var subForm = form.items[0]; - if (form.items.length > 1) { - subForm = { - type: 'section', - items: form.items.map(function(item) { - item.ngModelOptions = form.ngModelOptions; - if (angular.isUndefined(item.readonly)) { - item.readonly = form.readonly; - } - return item; - }) - }; - } - - } - - // We ceate copies of the form on demand, caching them for - // later requests - scope.copyWithIndex = function(index) { - if (!formDefCache[index]) { - if (subForm) { - var copy = angular.copy(subForm); - copy.arrayIndex = index; - schemaForm.traverseForm(copy, setIndex(index)); - formDefCache[index] = copy; - } - } - return formDefCache[index]; - }; - - scope.appendToArray = function() { - var len = list.length; - var copy = scope.copyWithIndex(len); - schemaForm.traverseForm(copy, function(part) { - - if (part.key) { - var def; - if (angular.isDefined(part['default'])) { - def = part['default']; - } - if (angular.isDefined(part.schema) && - angular.isDefined(part.schema['default'])) { - def = part.schema['default']; - } - - if (angular.isDefined(def)) { - sfSelect(part.key, scope.model, def); - } - } - }); - - // If there are no defaults nothing is added so we need to initialize - // the array. undefined for basic values, {} or [] for the others. - if (len === list.length) { - var type = sfSelect('schema.items.type', form); - var dflt; - if (type === 'object') { - dflt = {}; - } else if (type === 'array') { - dflt = []; - } - list.push(dflt); - } - - // Trigger validation. - scope.validateArray(); - return list; - }; - - scope.deleteFromArray = function(index) { - list.splice(index, 1); - - // Trigger validation. - scope.validateArray(); - - // Angular 1.2 lacks setDirty - if (ngModel && ngModel.$setDirty) { - ngModel.$setDirty(); - } - return list; - }; - - // Always start with one empty form unless configured otherwise. - // Special case: don't do it if form has a titleMap - if (!form.titleMap && form.startEmpty !== true && list.length === 0) { - scope.appendToArray(); - } - - // Title Map handling - // If form has a titleMap configured we'd like to enable looping over - // titleMap instead of modelArray, this is used for intance in - // checkboxes. So instead of variable number of things we like to create - // a array value from a subset of values in the titleMap. - // The problem here is that ng-model on a checkbox doesn't really map to - // a list of values. This is here to fix that. - if (form.titleMap && form.titleMap.length > 0) { - scope.titleMapValues = []; - - // We watch the model for changes and the titleMapValues to reflect - // the modelArray - var updateTitleMapValues = function(arr) { - scope.titleMapValues = []; - arr = arr || []; - - form.titleMap.forEach(function(item) { - scope.titleMapValues.push(arr.indexOf(item.value) !== -1); - }); - }; - //Catch default values - updateTitleMapValues(scope.modelArray); - scope.$watchCollection('modelArray', updateTitleMapValues); - - //To get two way binding we also watch our titleMapValues - scope.$watchCollection('titleMapValues', function(vals, old) { - if (vals && vals !== old) { - var arr = scope.modelArray; - - // Apparently the fastest way to clear an array, readable too. - // http://jsperf.com/array-destroy/32 - while (arr.length > 0) { - arr.pop(); - } - form.titleMap.forEach(function(item, index) { - if (vals[index]) { - arr.push(item.value); - } - }); - - // Time to validate the rebuilt array. - scope.validateArray(); - } - }); - } - - // If there is a ngModel present we need to validate when asked. - if (ngModel) { - var error; - - scope.validateArray = function() { - // The actual content of the array is validated by each field - // so we settle for checking validations specific to arrays - - // Since we prefill with empty arrays we can get the funny situation - // where the array is required but empty in the gui but still validates. - // Thats why we check the length. - var result = sfValidator.validate( - form, - scope.modelArray.length > 0 ? scope.modelArray : undefined - ); - - // TODO: DRY this up, it has a lot of similarities with schema-validate - // Since we might have different tv4 errors we must clear all - // errors that start with tv4- - Object.keys(ngModel.$error) - .filter(function(k) { return k.indexOf('tv4-') === 0; }) - .forEach(function(k) { ngModel.$setValidity(k, true); }); - - if (result.valid === false && - result.error && - (result.error.dataPath === '' || - result.error.dataPath === '/' + form.key[form.key.length - 1])) { - - // Set viewValue to trigger $dirty on field. If someone knows a - // a better way to do it please tell. - ngModel.$setViewValue(scope.modelArray); - error = result.error; - ngModel.$setValidity('tv4-' + result.error.code, false); - } - }; - - scope.$on('schemaFormValidate', scope.validateArray); - - scope.hasSuccess = function() { - return ngModel.$valid && !ngModel.$pristine; - }; - - scope.hasError = function() { - return ngModel.$invalid; - }; - - scope.schemaError = function() { - return error; - }; - - } - - once(); - }); - } - }; - } -]); - -/** - * A version of ng-changed that only listens if - * there is actually a onChange defined on the form - * - * Takes the form definition as argument. - * If the form definition has a "onChange" defined as either a function or - */ -angular.module('schemaForm').directive('sfChanged', function() { - return { - require: 'ngModel', - restrict: 'AC', - scope: false, - link: function(scope, element, attrs, ctrl) { - var form = scope.$eval(attrs.sfChanged); - //"form" is really guaranteed to be here since the decorator directive - //waits for it. But best be sure. - if (form && form.onChange) { - ctrl.$viewChangeListeners.push(function() { - if (angular.isFunction(form.onChange)) { - form.onChange(ctrl.$modelValue, form); - } else { - scope.evalExpr(form.onChange, {'modelValue': ctrl.$modelValue, form: form}); - } - }); - } - } - }; -}); - -angular.module('schemaForm').directive('sfField', - ['$parse', '$compile', '$http', '$templateCache', '$interpolate', '$q', 'sfErrorMessage', - 'sfPath','sfSelect', - function($parse, $compile, $http, $templateCache, $interpolate, $q, sfErrorMessage, - sfPath, sfSelect) { - - return { - restrict: 'AE', - replace: false, - transclude: false, - scope: true, - require: '?^sfSchema', - link: { - pre: function(scope) { - //The ngModelController is used in some templates and - //is needed for error messages, - scope.$on('schemaFormPropagateNgModelController', function(event, ngModel) { - event.stopPropagation(); - event.preventDefault(); - scope.ngModel = ngModel; - }); - }, - post: function(scope, element, attrs, sfSchema) { - - //Keep error prone logic from the template - scope.showTitle = function() { - return scope.form && scope.form.notitle !== true && scope.form.title; - }; - - scope.listToCheckboxValues = function(list) { - var values = {}; - angular.forEach(list, function(v) { - values[v] = true; - }); - return values; - }; - - scope.checkboxValuesToList = function(values) { - var lst = []; - angular.forEach(values, function(v, k) { - if (v) { - lst.push(k); - } - }); - return lst; - }; - - scope.buttonClick = function($event, form) { - if (angular.isFunction(form.onClick)) { - form.onClick($event, form); - } else if (angular.isString(form.onClick)) { - if (sfSchema) { - //evaluating in scope outside of sfSchemas isolated scope - sfSchema.evalInParentScope(form.onClick, {'$event': $event, form: form}); - } else { - scope.$eval(form.onClick, {'$event': $event, form: form}); - } - } - }; - - /** - * Evaluate an expression, i.e. scope.$eval - * but do it in sfSchemas parent scope sf-schema directive is used - * @param {string} expression - * @param {Object} locals (optional) - * @return {Any} the result of the expression - */ - scope.evalExpr = function(expression, locals) { - if (sfSchema) { - //evaluating in scope outside of sfSchemas isolated scope - return sfSchema.evalInParentScope(expression, locals); - } - - return scope.$eval(expression, locals); - }; - - /** - * Evaluate an expression, i.e. scope.$eval - * in this decorators scope - * @param {string} expression - * @param {Object} locals (optional) - * @return {Any} the result of the expression - */ - scope.evalInScope = function(expression, locals) { - if (expression) { - return scope.$eval(expression, locals); - } - }; - - /** - * Interpolate the expression. - * Similar to `evalExpr()` and `evalInScope()` - * but will not fail if the expression is - * text that contains spaces. - * - * Use the Angular `{{ interpolation }}` - * braces to access properties on `locals`. - * - * @param {string} content The string to interpolate. - * @param {Object} locals (optional) Properties that may be accessed in the - * `expression` string. - * @return {Any} The result of the expression or `undefined`. - */ - scope.interp = function(expression, locals) { - return (expression && $interpolate(expression)(locals)); - }; - - //This works since we ot the ngModel from the array or the schema-validate directive. - scope.hasSuccess = function() { - if (!scope.ngModel) { - return false; - } - return scope.ngModel.$valid && - (!scope.ngModel.$pristine || !scope.ngModel.$isEmpty(scope.ngModel.$modelValue)); - }; - - scope.hasError = function() { - if (!scope.ngModel) { - return false; - } - return scope.ngModel.$invalid && !scope.ngModel.$pristine; - }; - - /** - * DEPRECATED: use sf-messages instead. - * Error message handler - * An error can either be a schema validation message or a angular js validtion - * error (i.e. required) - */ - scope.errorMessage = function(schemaError) { - return sfErrorMessage.interpolate( - (schemaError && schemaError.code + '') || 'default', - (scope.ngModel && scope.ngModel.$modelValue) || '', - (scope.ngModel && scope.ngModel.$viewValue) || '', - scope.form, - scope.options && scope.options.validationMessage - ); - }; - - // Rebind our part of the form to the scope. - var once = scope.$watch(attrs.sfField, function(form) { - if (form) { - // Workaround for 'updateOn' error from ngModelOptions - // see https://github.com/Textalk/angular-schema-form/issues/255 - // and https://github.com/Textalk/angular-schema-form/issues/206 - form.ngModelOptions = form.ngModelOptions || {}; - scope.form = form; - - - // Where there is a key there is probably a ngModel - if (form.key) { - // It looks better with dot notation. - scope.$on( - 'schemaForm.error.' + form.key.join('.'), - function(event, error, validationMessage, validity) { - if (validationMessage === true || validationMessage === false) { - validity = validationMessage; - validationMessage = undefined; - } - - if (scope.ngModel && error) { - if (scope.ngModel.$setDirty) { - scope.ngModel.$setDirty(); - } else { - // FIXME: Check that this actually works on 1.2 - scope.ngModel.$dirty = true; - scope.ngModel.$pristine = false; - } - - // Set the new validation message if one is supplied - // Does not work when validationMessage is just a string. - if (validationMessage) { - if (!form.validationMessage) { - form.validationMessage = {}; - } - form.validationMessage[error] = validationMessage; - } - - scope.ngModel.$setValidity(error, validity === true); - - if (validity === true) { - // Setting or removing a validity can change the field to believe its valid - // but its not. So lets trigger its validation as well. - scope.$broadcast('schemaFormValidate'); - } - } - }); - - // Clean up the model when the corresponding form field is $destroy-ed. - // Default behavior can be supplied as a globalOption, and behavior can be overridden in the form definition. - scope.$on('$destroy', function() { - // If the entire schema form is destroyed we don't touch the model - if (!scope.externalDestructionInProgress) { - var destroyStrategy = form.destroyStrategy || - (scope.options && scope.options.destroyStrategy) || 'remove'; - // No key no model, and we might have strategy 'retain' - if (form.key && destroyStrategy !== 'retain') { - - // Get the object that has the property we wan't to clear. - var obj = scope.model; - if (form.key.length > 1) { - obj = sfSelect(form.key.slice(0, form.key.length - 1), obj); - } - - // We can get undefined here if the form hasn't been filled out entirely - if (obj === undefined) { - return; - } - - // Type can also be a list in JSON Schema - var type = (form.schema && form.schema.type) || ''; - - // Empty means '',{} and [] for appropriate types and undefined for the rest - //console.log('destroy', destroyStrategy, form.key, type, obj); - if (destroyStrategy === 'empty' && type.indexOf('string') !== -1) { - obj[form.key.slice(-1)] = ''; - } else if (destroyStrategy === 'empty' && type.indexOf('object') !== -1) { - obj[form.key.slice(-1)] = {}; - } else if (destroyStrategy === 'empty' && type.indexOf('array') !== -1) { - obj[form.key.slice(-1)] = []; - } else if (destroyStrategy === 'null') { - obj[form.key.slice(-1)] = null; - } else { - delete obj[form.key.slice(-1)]; - } - } - } - }); - } - - once(); - } - }); - } - } - }; - } - ]); - -angular.module('schemaForm').directive('sfMessage', -['$injector', 'sfErrorMessage', function($injector, sfErrorMessage) { - return { - scope: false, - restrict: 'EA', - link: function(scope, element, attrs) { - - //Inject sanitizer if it exists - var $sanitize = $injector.has('$sanitize') ? - $injector.get('$sanitize') : function(html) { return html; }; - - var message = ''; - - if (attrs.sfMessage) { - scope.$watch(attrs.sfMessage, function(msg) { - if (msg) { - message = $sanitize(msg); - if (scope.ngModel) { - update(scope.ngModel.$valid); - } else { - update(); - } - } - }); - } - - var update = function(valid) { - if (valid && !scope.hasError()) { - element.html(message); - } else { - - - var errors = []; - angular.forEach(((scope.ngModel && scope.ngModel.$error) || {}), function(status, code) { - if (status) { - // if true then there is an error - // Angular 1.3 removes properties, so we will always just have errors. - // Angular 1.2 sets them to false. - errors.push(code); - } - }); - - // In Angular 1.3 we use one $validator to stop the model value from getting updated. - // this means that we always end up with a 'schemaForm' error. - errors = errors.filter(function(e) { return e !== 'schemaForm'; }); - - // We only show one error. - // TODO: Make that optional - var error = errors[0]; - if (error) { - element.html(sfErrorMessage.interpolate( - error, - scope.ngModel.$modelValue, - scope.ngModel.$viewValue, - scope.form, - scope.options && scope.options.validationMessage - )); - } else { - element.html(message); - } - } - }; - update(); - - scope.$watchCollection('ngModel.$error', function() { - if (scope.ngModel) { - update(scope.ngModel.$valid); - } - }); - - } - }; -}]); - -/* -FIXME: real documentation -
-*/ - -angular.module('schemaForm') - .directive('sfSchema', -['$compile', 'schemaForm', 'schemaFormDecorators', 'sfSelect', 'sfPath', 'sfBuilder', - function($compile, schemaForm, schemaFormDecorators, sfSelect, sfPath, sfBuilder) { - - return { - scope: { - schema: '=sfSchema', - initialForm: '=sfForm', - model: '=sfModel', - options: '=sfOptions' - }, - controller: ['$scope', function($scope) { - this.evalInParentScope = function(expr, locals) { - return $scope.$parent.$eval(expr, locals); - }; - }], - replace: false, - restrict: 'A', - transclude: true, - require: '?form', - link: function(scope, element, attrs, formCtrl, transclude) { - - //expose form controller on scope so that we don't force authors to use name on form - scope.formCtrl = formCtrl; - - //We'd like to handle existing markup, - //besides using it in our template we also - //check for ng-model and add that to an ignore list - //i.e. even if form has a definition for it or form is ["*"] - //we don't generate it. - var ignore = {}; - transclude(scope, function(clone) { - clone.addClass('schema-form-ignore'); - element.prepend(clone); - - if (element[0].querySelectorAll) { - var models = element[0].querySelectorAll('[ng-model]'); - if (models) { - for (var i = 0; i < models.length; i++) { - var key = models[i].getAttribute('ng-model'); - //skip first part before . - ignore[key.substring(key.indexOf('.') + 1)] = true; - } - } - } - }); - - var lastDigest = {}; - var childScope; - - // Common renderer function, can either be triggered by a watch or by an event. - var render = function(schema, form) { - var merged = schemaForm.merge(schema, form, ignore, scope.options); - - // Create a new form and destroy the old one. - // Not doing keeps old form elements hanging around after - // they have been removed from the DOM - // https://github.com/Textalk/angular-schema-form/issues/200 - if (childScope) { - // Destroy strategy should not be acted upon - scope.externalDestructionInProgress = true; - childScope.$destroy(); - scope.externalDestructionInProgress = false; - } - childScope = scope.$new(); - - //make the form available to decorators - childScope.schemaForm = {form: merged, schema: schema}; - - //clean all but pre existing html. - element.children(':not(.schema-form-ignore)').remove(); - - // Find all slots. - var slots = {}; - var slotsFound = element[0].querySelectorAll('*[sf-insert-field]'); - - for (var i = 0; i < slotsFound.length; i++) { - slots[slotsFound[i].getAttribute('sf-insert-field')] = slotsFound[i]; - } - - // if sfUseDecorator is undefined the default decorator is used. - var decorator = schemaFormDecorators.decorator(attrs.sfUseDecorator); - - // Use the builder to build it and append the result - element[0].appendChild( sfBuilder.build(merged, decorator, slots) ); - - //compile only children - $compile(element.children())(childScope); - - //ok, now that that is done let's set any defaults - if (!scope.options || scope.options.setSchemaDefaults !== false) { - schemaForm.traverseSchema(schema, function(prop, path) { - if (angular.isDefined(prop['default'])) { - var val = sfSelect(path, scope.model); - if (angular.isUndefined(val)) { - sfSelect(path, scope.model, prop['default']); - } - } - }); - } - - scope.$emit('sf-render-finished', element); - }; - - //Since we are dependant on up to three - //attributes we'll do a common watch - scope.$watch(function() { - - var schema = scope.schema; - var form = scope.initialForm || ['*']; - - //The check for schema.type is to ensure that schema is not {} - if (form && schema && schema.type && - (lastDigest.form !== form || lastDigest.schema !== schema) && - Object.keys(schema.properties).length > 0) { - lastDigest.schema = schema; - lastDigest.form = form; - - render(schema, form); - } - }); - - // We also listen to the event schemaFormRedraw so you can manually trigger a change if - // part of the form or schema is chnaged without it being a new instance. - scope.$on('schemaFormRedraw', function() { - var schema = scope.schema; - var form = scope.initialForm || ['*']; - if (schema) { - render(schema, form); - } - }); - - scope.$on('$destroy', function() { - // Each field listens to the $destroy event so that it can remove any value - // from the model if that field is removed from the form. This is the default - // destroy strategy. But if the entire form (or at least the part we're on) - // gets removed, like when routing away to another page, then we definetly want to - // keep the model intact. So therefore we set a flag to tell the others it's time to just - // let it be. - scope.externalDestructionInProgress = true; - }); - } - }; - } -]); - -angular.module('schemaForm').directive('schemaValidate', ['sfValidator', '$parse', 'sfSelect', - function(sfValidator, $parse, sfSelect) { - - return { - restrict: 'A', - scope: false, - // We want the link function to be *after* the input directives link function so we get access - // the parsed value, ex. a number instead of a string - priority: 500, - require: 'ngModel', - link: function(scope, element, attrs, ngModel) { - - // We need the ngModelController on several places, - // most notably for errors. - // So we emit it up to the decorator directive so it can put it on scope. - scope.$emit('schemaFormPropagateNgModelController', ngModel); - - var error = null; - - // When using the new builder we might not have form just yet - var once = scope.$watch(attrs.schemaValidate, function(form) { - if (!form) { - return; - } - - if (form.copyValueTo) { - ngModel.$viewChangeListeners.push(function() { - var paths = form.copyValueTo; - angular.forEach(paths, function(path) { - sfSelect(path, scope.model, ngModel.$modelValue); - }); - }); - } - - // Validate against the schema. - - var validate = function(viewValue) { - //Still might be undefined - if (!form) { - return viewValue; - } - - // Omit TV4 validation - if (scope.options && scope.options.tv4Validation === false) { - return viewValue; - } - - var result = sfValidator.validate(form, viewValue); - - - // Since we might have different tv4 errors we must clear all - // errors that start with tv4- - Object.keys(ngModel.$error) - .filter(function(k) { return k.indexOf('tv4-') === 0; }) - .forEach(function(k) { ngModel.$setValidity(k, true); }); - - if (!result.valid) { - // it is invalid, return undefined (no model update) - ngModel.$setValidity('tv4-' + result.error.code, false); - error = result.error; - - // In Angular 1.3+ return the viewValue, otherwise we inadvertenly - // will trigger a 'parse' error. - // we will stop the model value from updating with our own $validator - // later. - if (ngModel.$validators) { - return viewValue; - } - // Angular 1.2 on the other hand lacks $validators and don't add a 'parse' error. - return undefined; - } - return viewValue; - }; - - // Custom validators, parsers, formatters etc - if (typeof form.ngModel === 'function') { - form.ngModel(ngModel); - } - - ['$parsers', '$viewChangeListeners', '$formatters'].forEach(function(attr) { - if (form[attr] && ngModel[attr]) { - form[attr].forEach(function(fn) { - ngModel[attr].push(fn); - }); - } - }); - - ['$validators', '$asyncValidators'].forEach(function(attr) { - // Check if our version of angular has validators, i.e. 1.3+ - if (form[attr] && ngModel[attr]) { - angular.forEach(form[attr], function(fn, name) { - ngModel[attr][name] = fn; - }); - } - }); - - // Get in last of the parses so the parsed value has the correct type. - // We don't use $validators since we like to set different errors depending tv4 error codes - ngModel.$parsers.push(validate); - - // But we do use one custom validator in the case of Angular 1.3 to stop the model from - // updating if we've found an error. - if (ngModel.$validators) { - ngModel.$validators.schemaForm = function() { - // Any error and we're out of here! - return !Object.keys(ngModel.$error).some(function(e) { return e !== 'schemaForm'}); - } - } - - // Listen to an event so we can validate the input on request - scope.$on('schemaFormValidate', function() { - - // We set the viewValue to trigger parsers, - // since modelValue might be empty and validating just that - // might change an existing error to a "required" error message. - if (ngModel.$setDirty) { - - // Angular 1.3+ - ngModel.$setDirty(); - ngModel.$setViewValue(ngModel.$viewValue); - ngModel.$commitViewValue(); - - // In Angular 1.3 setting undefined as a viewValue does not trigger parsers - // so we need to do a special required check. Fortunately we have $isEmpty - if (form.required && ngModel.$isEmpty(ngModel.$modelValue)) { - ngModel.$setValidity('tv4-302', false); - } - - } else { - // Angular 1.2 - // In angular 1.2 setting a viewValue of undefined will trigger the parser. - // hence required works. - ngModel.$setViewValue(ngModel.$viewValue); - } - - }); - - scope.schemaError = function() { - return error; - }; - - // Just watch once. - once(); - }); - } - }; - }]); - -return schemaForm; -})); diff --git a/dist/schema-form.min.js b/dist/schema-form.min.js deleted file mode 100644 index 7f85995c0..000000000 --- a/dist/schema-form.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(e,t){"function"==typeof define&&define.amd?define(["angular","ObjectPath","tv4"],t):"object"==typeof exports?module.exports=t(require("angular"),require("ObjectPath"),require("tv4")):e.schemaForm=t(e.angular,e.ObjectPath,e.tv4)}(this,function(e,t,r){var n=[];try{e.module("ngSanitize"),n.push("ngSanitize")}catch(i){}try{e.module("ui.sortable"),n.push("ui.sortable")}catch(i){}try{e.module("angularSpectrumColorpicker"),n.push("angularSpectrumColorpicker")}catch(i){}var o=e.module("schemaForm",n);return e.module("schemaForm").provider("sfPath",[function(){var r={parse:t.parse};1===e.version.major&&e.version.minor<3?r.stringify=function(e){return Array.isArray(e)?e.join("."):e.toString()}:r.stringify=t.stringify,r.normalize=function(e,t){return r.stringify(Array.isArray(e)?e:r.parse(e),t)},this.parse=r.parse,this.stringify=r.stringify,this.normalize=r.normalize,this.$get=function(){return r}}]),e.module("schemaForm").factory("sfSelect",["sfPath",function(e){var t=/^\d+$/;return function(r,n,i){n||(n=this);var o="string"==typeof r?e.parse(r):r;if("undefined"!=typeof i&&1===o.length)return n[o[0]]=i,n;"undefined"!=typeof i&&"undefined"==typeof n[o[0]]&&(n[o[0]]=o.length>2&&t.test(o[1])?[]:{});for(var a=n[o[0]],l=1;l0;)m.appendChild(d.childNodes[0]);m.firstChild.setAttribute("sf-field",s+"["+u+"]"),f.builder({fieldFrag:m,form:c,path:s+"["+u+"]",build:function(e,r){return a(e,t,n,l,r)}}),(o(c,l)||e).appendChild(m)}else{var v=document.createElement(i(t.__name,"-"));v.setAttribute("form",s+"["+u+"]"),(o(c,l)||e).appendChild(v)}return e}},c),c},l={build:function(t,r,n){return a(t,r,function(t){return e.get(t)||""},n)},internalBuild:a};return l}]),e.module("schemaForm").provider("schemaFormDecorators",["$compileProvider","sfPathProvider",function(t,r){var n="",i={},o=function(e,t){"sfDecorator"===e&&(e=n);var r=i[e];return r[t.type]?r[t.type].template:r["default"].template},a=function(n){t.directive(n,["$parse","$compile","$http","$templateCache","$interpolate","$q","sfErrorMessage","sfPath","sfSelect",function(t,i,a,l,s,c,u,f,m){return{restrict:"AE",replace:!1,transclude:!1,scope:!0,require:"?^sfSchema",link:function(t,d,p,h){t.$on("schemaFormPropagateNgModelController",function(e,r){e.stopPropagation(),e.preventDefault(),t.ngModel=r}),t.showTitle=function(){return t.form&&t.form.notitle!==!0&&t.form.title},t.listToCheckboxValues=function(t){var r={};return e.forEach(t,function(e){r[e]=!0}),r},t.checkboxValuesToList=function(t){var r=[];return e.forEach(t,function(e,t){e&&r.push(t)}),r},t.buttonClick=function(r,n){e.isFunction(n.onClick)?n.onClick(r,n):e.isString(n.onClick)&&(h?h.evalInParentScope(n.onClick,{$event:r,form:n}):t.$eval(n.onClick,{$event:r,form:n}))},t.evalExpr=function(e,r){return h?h.evalInParentScope(e,r):t.$eval(e,r)},t.evalInScope=function(e,r){return e?t.$eval(e,r):void 0},t.interp=function(e,t){return e&&s(e)(t)},t.hasSuccess=function(){return t.ngModel?t.ngModel.$valid&&(!t.ngModel.$pristine||!t.ngModel.$isEmpty(t.ngModel.$modelValue)):!1},t.hasError=function(){return t.ngModel?t.ngModel.$invalid&&!t.ngModel.$pristine:!1},t.errorMessage=function(e){return u.interpolate(e&&e.code+""||"default",t.ngModel&&t.ngModel.$modelValue||"",t.ngModel&&t.ngModel.$viewValue||"",t.form,t.options&&t.options.validationMessage)};var v=t.$watch(p.form,function(s){if(s){s.ngModelOptions=s.ngModelOptions||{},t.form=s;var u;if("template"===s.type&&s.template)u=c.when(s.template);else{var p="template"===s.type?s.templateUrl:o(n,s);u=a.get(p,{cache:l}).then(function(e){return e.data})}u.then(function(n){if(s.key){var o=s.key?r.stringify(s.key).replace(/"/g,"""):"";n=n.replace(/\$\$value\$\$/g,"model"+("["!==o[0]?".":"")+o)}if(d.html(n),s.condition){var a='evalExpr(form.condition,{ model: model, "arrayIndex": arrayIndex})';s.key&&(a='evalExpr(form.condition,{ model: model, "arrayIndex": arrayIndex, "modelValue": model'+f.stringify(s.key)+"})"),e.forEach(d.children(),function(e){var t=e.getAttribute("ng-if");e.setAttribute("ng-if",t?"("+t+") || ("+a+")":a)})}i(d.contents())(t)}),s.key&&(t.$on("schemaForm.error."+s.key.join("."),function(e,r,n,i){(n===!0||n===!1)&&(i=n,n=void 0),t.ngModel&&r&&(t.ngModel.$setDirty?t.ngModel.$setDirty():(t.ngModel.$dirty=!0,t.ngModel.$pristine=!1),n&&(s.validationMessage||(s.validationMessage={}),s.validationMessage[r]=n),t.ngModel.$setValidity(r,i===!0),i===!0&&t.$broadcast("schemaFormValidate"))}),t.$on("$destroy",function(){if(!t.externalDestructionInProgress){var e=s.destroyStrategy||t.options&&t.options.destroyStrategy||"remove";if(s.key&&"retain"!==e){var r=t.model;if(s.key.length>1&&(r=m(s.key.slice(0,s.key.length-1),r)),void 0===r)return;var n=s.schema&&s.schema.type||"";"empty"===e&&-1!==n.indexOf("string")?r[s.key.slice(-1)]="":"empty"===e&&-1!==n.indexOf("object")?r[s.key.slice(-1)]={}:"empty"===e&&-1!==n.indexOf("array")?r[s.key.slice(-1)]=[]:"null"===e?r[s.key.slice(-1)]=null:delete r[s.key.slice(-1)]}}})),v()}})}}}])},l=function(r,n,i){i=e.isDefined(i)?i:!1,t.directive("sf"+e.uppercase(r[0])+r.substr(1),function(){return{restrict:"EAC",scope:!0,replace:!0,transclude:i,template:'',link:function(t,n,i){var o={items:"c",titleMap:"c",schema:"c"},a={type:r},l=!0;e.forEach(i,function(r,n){if("$"!==n[0]&&0!==n.indexOf("ng")&&"sfField"!==n){var s=function(r){e.isDefined(r)&&r!==a[n]&&(a[n]=r,l&&a.type&&(a.key||e.isUndefined(i.key))&&(t.form=a,l=!1))};"model"===n?t.$watch(r,function(e){e&&t.model!==e&&(t.model=e)}):"c"===o[n]?t.$watchCollection(r,s):i.$observe(n,s)}})}}})};this.createDecorator=function(t,r){console.warn("schemaFormDecorators.createDecorator is DEPRECATED, use defineDecorator instead."),i[t]={__name:t},e.forEach(r,function(r,n){i[t][n]={template:r,replace:!1,builder:e.noop}}),i[n]||(n=t),a(t)},this.defineDecorator=function(t,r){i[t]={__name:t},e.forEach(r,function(r,n){r.builder=r.builder||e.noop,r.replace=e.isDefined(r.replace)?r.replace:!0,i[t][n]=r}),i[n]||(n=t),a(t)},this.createDirective=l,this.createDirectives=function(t){e.forEach(t,function(e,t){l(t,e)})},this.decorator=function(e){return e=e||n,i[e]},this.addMapping=function(e,t,r,n,o){i[e]&&(i[e][t]={template:r,builder:n,replace:!!o})},this.$get=function(){return{decorator:function(e){return i[e]||i[n]},defaultDecorator:n}},a("sfDecorator")}]),e.module("schemaForm").provider("sfErrorMessage",function(){var t={"default":"Field does not validate",0:"Invalid type, expected {{schema.type}}",1:"No enum match for: {{value}}",10:'Data does not match any schemas from "anyOf"',11:'Data does not match any schemas from "oneOf"',12:'Data is valid against more than one schema from "oneOf"',13:'Data matches schema from "not"',100:"Value is not a multiple of {{schema.divisibleBy}}",101:"{{viewValue}} is less than the allowed minimum of {{schema.minimum}}",102:"{{viewValue}} is equal to the exclusive minimum {{schema.minimum}}",103:"{{viewValue}} is greater than the allowed maximum of {{schema.maximum}}",104:"{{viewValue}} is equal to the exclusive maximum {{schema.maximum}}",105:"Value is not a valid number",200:"String is too short ({{viewValue.length}} chars), minimum {{schema.minLength}}",201:"String is too long ({{viewValue.length}} chars), maximum {{schema.maxLength}}",202:"String does not match pattern: {{schema.pattern}}",300:"Too few properties defined, minimum {{schema.minProperties}}",301:"Too many properties defined, maximum {{schema.maxProperties}}",302:"Required",303:"Additional properties not allowed",304:"Dependency failed - key must exist",400:"Array is too short ({{value.length}}), minimum {{schema.maxItems}}",401:"Array is too long ({{value.length}}), maximum {{schema.minItems}}",402:"Array items are not unique",403:"Additional items not allowed",500:"Format validation failed",501:'Keyword failed: "{{title}}"',600:"Circular $refs",1e3:"Unknown property (not in schema)"};t.number=t[105],t.required=t[302],t.min=t[101],t.max=t[103],t.maxlength=t[201],t.minlength=t[200],t.pattern=t[202],this.setDefaultMessages=function(e){t=e},this.getDefaultMessages=function(){return t},this.setDefaultMessage=function(e,r){t[e]=r},this.$get=["$interpolate",function(r){var n={};return n.defaultMessages=t,n.interpolate=function(n,i,o,a,l){l=l||{};var s=a.validationMessage||{};0===n.indexOf("tv4-")&&(n=n.substring(4));var c=s["default"]||l["default"]||"";[s,l,t].some(function(t){return e.isString(t)||e.isFunction(t)?(c=t,!0):t&&t[n]?(c=t[n],!0):void 0});var u={error:n,value:i,viewValue:o,form:a,schema:a.schema,title:a.title||a.schema&&a.schema.title};return e.isFunction(c)?c(u):r(c)(u)},n}]}),e.module("schemaForm").provider("schemaForm",["sfPathProvider",function(t){var r=function(e){if(Array.isArray(e)&&2==e.length){if("null"===e[0])return e[1];if("null"===e[1])return e[0]}return e},n=function(e){var t=[];return e.forEach(function(e){t.push({name:e,value:e})}),t},i=function(t,r){if(!e.isArray(t)){var n=[];return r?e.forEach(r,function(e,r){n.push({name:t[e],value:e})}):e.forEach(t,function(e,t){n.push({name:e,value:t})}),n}return t},o=function(t,n,i){var o=h[r(n.type)];if(o)for(var a,l=0;l1&&(m={type:"section",items:l.items.map(function(t){return t.ngModelOptions=l.ngModelOptions,e.isUndefined(t.readonly)&&(t.readonly=l.readonly),t})})}if(a.copyWithIndex=function(t){if(!u[t]&&m){var n=e.copy(m);n.arrayIndex=t,r.traverseForm(n,o(t)),u[t]=n}return u[t]},a.appendToArray=function(){var n=s.length,i=a.copyWithIndex(n);if(r.traverseForm(i,function(r){if(r.key){var n;e.isDefined(r["default"])&&(n=r["default"]),e.isDefined(r.schema)&&e.isDefined(r.schema["default"])&&(n=r.schema["default"]),e.isDefined(n)&&t(r.key,a.model,n)}}),n===s.length){var o,c=t("schema.items.type",l);"object"===c?o={}:"array"===c&&(o=[]),s.push(o)}return a.validateArray(),s},a.deleteFromArray=function(e){return s.splice(e,1),a.validateArray(),c&&c.$setDirty&&c.$setDirty(),s},l.titleMap||l.startEmpty===!0||0!==s.length||a.appendToArray(),l.titleMap&&l.titleMap.length>0){a.titleMapValues=[];var d=function(e){a.titleMapValues=[],e=e||[],l.titleMap.forEach(function(t){a.titleMapValues.push(-1!==e.indexOf(t.value))})};d(a.modelArray),a.$watchCollection("modelArray",d),a.$watchCollection("titleMapValues",function(e,t){if(e&&e!==t){for(var r=a.modelArray;r.length>0;)r.pop();l.titleMap.forEach(function(t,n){e[n]&&r.push(t.value)}),a.validateArray()}})}if(c){var p;a.validateArray=function(){var e=n.validate(l,a.modelArray.length>0?a.modelArray:void 0);Object.keys(c.$error).filter(function(e){return 0===e.indexOf("tv4-")}).forEach(function(e){c.$setValidity(e,!0)}),e.valid!==!1||!e.error||""!==e.error.dataPath&&e.error.dataPath!=="/"+l.key[l.key.length-1]||(c.$setViewValue(a.modelArray),p=e.error,c.$setValidity("tv4-"+e.error.code,!1))},a.$on("schemaFormValidate",a.validateArray),a.hasSuccess=function(){return c.$valid&&!c.$pristine},a.hasError=function(){return c.$invalid},a.schemaError=function(){return p}}f()})}}}]),e.module("schemaForm").directive("sfChanged",function(){return{require:"ngModel",restrict:"AC",scope:!1,link:function(t,r,n,i){var o=t.$eval(n.sfChanged);o&&o.onChange&&i.$viewChangeListeners.push(function(){e.isFunction(o.onChange)?o.onChange(i.$modelValue,o):t.evalExpr(o.onChange,{modelValue:i.$modelValue,form:o})})}}}),e.module("schemaForm").directive("sfField",["$parse","$compile","$http","$templateCache","$interpolate","$q","sfErrorMessage","sfPath","sfSelect",function(t,r,n,i,o,a,l,s,c){return{restrict:"AE",replace:!1,transclude:!1,scope:!0,require:"?^sfSchema",link:{pre:function(e){e.$on("schemaFormPropagateNgModelController",function(t,r){t.stopPropagation(),t.preventDefault(),e.ngModel=r})},post:function(t,r,n,i){t.showTitle=function(){return t.form&&t.form.notitle!==!0&&t.form.title},t.listToCheckboxValues=function(t){var r={};return e.forEach(t,function(e){r[e]=!0}),r},t.checkboxValuesToList=function(t){var r=[];return e.forEach(t,function(e,t){e&&r.push(t)}),r},t.buttonClick=function(r,n){e.isFunction(n.onClick)?n.onClick(r,n):e.isString(n.onClick)&&(i?i.evalInParentScope(n.onClick,{$event:r,form:n}):t.$eval(n.onClick,{$event:r,form:n}))},t.evalExpr=function(e,r){return i?i.evalInParentScope(e,r):t.$eval(e,r)},t.evalInScope=function(e,r){return e?t.$eval(e,r):void 0},t.interp=function(e,t){return e&&o(e)(t)},t.hasSuccess=function(){return t.ngModel?t.ngModel.$valid&&(!t.ngModel.$pristine||!t.ngModel.$isEmpty(t.ngModel.$modelValue)):!1},t.hasError=function(){return t.ngModel?t.ngModel.$invalid&&!t.ngModel.$pristine:!1},t.errorMessage=function(e){return l.interpolate(e&&e.code+""||"default",t.ngModel&&t.ngModel.$modelValue||"",t.ngModel&&t.ngModel.$viewValue||"",t.form,t.options&&t.options.validationMessage)};var a=t.$watch(n.sfField,function(e){e&&(e.ngModelOptions=e.ngModelOptions||{},t.form=e,e.key&&(t.$on("schemaForm.error."+e.key.join("."),function(r,n,i,o){(i===!0||i===!1)&&(o=i,i=void 0),t.ngModel&&n&&(t.ngModel.$setDirty?t.ngModel.$setDirty():(t.ngModel.$dirty=!0,t.ngModel.$pristine=!1),i&&(e.validationMessage||(e.validationMessage={}),e.validationMessage[n]=i),t.ngModel.$setValidity(n,o===!0),o===!0&&t.$broadcast("schemaFormValidate"))}),t.$on("$destroy",function(){if(!t.externalDestructionInProgress){var r=e.destroyStrategy||t.options&&t.options.destroyStrategy||"remove";if(e.key&&"retain"!==r){var n=t.model;if(e.key.length>1&&(n=c(e.key.slice(0,e.key.length-1),n)),void 0===n)return;var i=e.schema&&e.schema.type||"";"empty"===r&&-1!==i.indexOf("string")?n[e.key.slice(-1)]="":"empty"===r&&-1!==i.indexOf("object")?n[e.key.slice(-1)]={}:"empty"===r&&-1!==i.indexOf("array")?n[e.key.slice(-1)]=[]:"null"===r?n[e.key.slice(-1)]=null:delete n[e.key.slice(-1)]}}})),a())})}}}}]),e.module("schemaForm").directive("sfMessage",["$injector","sfErrorMessage",function(t,r){return{scope:!1,restrict:"EA",link:function(n,i,o){var a=t.has("$sanitize")?t.get("$sanitize"):function(e){return e},l="";o.sfMessage&&n.$watch(o.sfMessage,function(e){e&&(l=a(e),n.ngModel?s(n.ngModel.$valid):s())});var s=function(t){if(t&&!n.hasError())i.html(l);else{var o=[];e.forEach(n.ngModel&&n.ngModel.$error||{},function(e,t){e&&o.push(t)}),o=o.filter(function(e){return"schemaForm"!==e});var a=o[0];a?i.html(r.interpolate(a,n.ngModel.$modelValue,n.ngModel.$viewValue,n.form,n.options&&n.options.validationMessage)):i.html(l)}};s(),n.$watchCollection("ngModel.$error",function(){n.ngModel&&s(n.ngModel.$valid)})}}}]),e.module("schemaForm").directive("sfSchema",["$compile","schemaForm","schemaFormDecorators","sfSelect","sfPath","sfBuilder",function(t,r,n,i,o,a){return{scope:{schema:"=sfSchema",initialForm:"=sfForm",model:"=sfModel",options:"=sfOptions"},controller:["$scope",function(e){this.evalInParentScope=function(t,r){return e.$parent.$eval(t,r)}}],replace:!1,restrict:"A",transclude:!0,require:"?form",link:function(o,l,s,c,u){o.formCtrl=c;var f={};u(o,function(e){if(e.addClass("schema-form-ignore"),l.prepend(e),l[0].querySelectorAll){var t=l[0].querySelectorAll("[ng-model]");if(t)for(var r=0;r0&&(d.schema=e,d.form=t,p(e,t))}),o.$on("schemaFormRedraw",function(){var e=o.schema,t=o.initialForm||["*"];e&&p(e,t)}),o.$on("$destroy",function(){o.externalDestructionInProgress=!0})}}}]),e.module("schemaForm").directive("schemaValidate",["sfValidator","$parse","sfSelect",function(t,r,n){return{restrict:"A",scope:!1,priority:500,require:"ngModel",link:function(r,i,o,a){r.$emit("schemaFormPropagateNgModelController",a);var l=null,s=r.$watch(o.schemaValidate,function(i){if(i){i.copyValueTo&&a.$viewChangeListeners.push(function(){var t=i.copyValueTo;e.forEach(t,function(e){n(e,r.model,a.$modelValue)})});var o=function(e){if(!i)return e;if(r.options&&r.options.tv4Validation===!1)return e;var n=t.validate(i,e);return Object.keys(a.$error).filter(function(e){return 0===e.indexOf("tv4-")}).forEach(function(e){a.$setValidity(e,!0)}),n.valid?e:(a.$setValidity("tv4-"+n.error.code,!1),l=n.error,a.$validators?e:void 0)};"function"==typeof i.ngModel&&i.ngModel(a),["$parsers","$viewChangeListeners","$formatters"].forEach(function(e){i[e]&&a[e]&&i[e].forEach(function(t){a[e].push(t)})}),["$validators","$asyncValidators"].forEach(function(t){i[t]&&a[t]&&e.forEach(i[t],function(e,r){a[t][r]=e})}),a.$parsers.push(o),a.$validators&&(a.$validators.schemaForm=function(){return!Object.keys(a.$error).some(function(e){return"schemaForm"!==e})}),r.$on("schemaFormValidate",function(){a.$setDirty?(a.$setDirty(),a.$setViewValue(a.$viewValue),a.$commitViewValue(),i.required&&a.$isEmpty(a.$modelValue)&&a.$setValidity("tv4-302",!1)):a.$setViewValue(a.$viewValue)}),r.schemaError=function(){return l},s()}})}}}]),o}); \ No newline at end of file diff --git a/src/directives/decorators/bootstrap/array.html b/src/directives/decorators/bootstrap/array.html index 8749b1872..b7871856d 100644 --- a/src/directives/decorators/bootstrap/array.html +++ b/src/directives/decorators/bootstrap/array.html @@ -22,7 +22,7 @@

{{ form.title }}

{{ form.add || 'Add'}} -
diff --git a/src/directives/decorators/bootstrap/checkbox.html b/src/directives/decorators/bootstrap/checkbox.html index d6ad64d4b..e11f60d51 100644 --- a/src/directives/decorators/bootstrap/checkbox.html +++ b/src/directives/decorators/bootstrap/checkbox.html @@ -11,5 +11,5 @@ name="{{form.key.slice(-1)[0]}}"> -
+
diff --git a/src/directives/decorators/bootstrap/checkboxes.html b/src/directives/decorators/bootstrap/checkboxes.html index 45b514135..6057ce99b 100644 --- a/src/directives/decorators/bootstrap/checkboxes.html +++ b/src/directives/decorators/bootstrap/checkboxes.html @@ -14,5 +14,5 @@ -
+
diff --git a/src/directives/decorators/bootstrap/default.html b/src/directives/decorators/bootstrap/default.html index b5685042b..8b5c15f32 100644 --- a/src/directives/decorators/bootstrap/default.html +++ b/src/directives/decorators/bootstrap/default.html @@ -50,5 +50,5 @@ id="{{form.key.slice(-1)[0] + 'Status'}}" class="sr-only">{{ hasSuccess() ? '(success)' : '(error)' }} -
+
diff --git a/src/directives/decorators/bootstrap/fieldset-trcl.html b/src/directives/decorators/bootstrap/fieldset-trcl.html index e4069bd77..31efa521e 100644 --- a/src/directives/decorators/bootstrap/fieldset-trcl.html +++ b/src/directives/decorators/bootstrap/fieldset-trcl.html @@ -1,5 +1,5 @@
{{ form.title }} -
+
diff --git a/src/directives/decorators/bootstrap/fieldset.html b/src/directives/decorators/bootstrap/fieldset.html index 4db3f059b..0248ea9bd 100644 --- a/src/directives/decorators/bootstrap/fieldset.html +++ b/src/directives/decorators/bootstrap/fieldset.html @@ -1,5 +1,5 @@
{{ form.title }} -
+
diff --git a/src/directives/decorators/bootstrap/radio-buttons.html b/src/directives/decorators/bootstrap/radio-buttons.html index 5a12dc86a..5c369d946 100644 --- a/src/directives/decorators/bootstrap/radio-buttons.html +++ b/src/directives/decorators/bootstrap/radio-buttons.html @@ -20,5 +20,5 @@ -
+
diff --git a/src/directives/decorators/bootstrap/radios-inline.html b/src/directives/decorators/bootstrap/radios-inline.html index 6c3d07928..81a264c82 100644 --- a/src/directives/decorators/bootstrap/radios-inline.html +++ b/src/directives/decorators/bootstrap/radios-inline.html @@ -14,5 +14,5 @@ -
+
diff --git a/src/directives/decorators/bootstrap/radios.html b/src/directives/decorators/bootstrap/radios.html index f3b73189b..4b9e1cf3f 100644 --- a/src/directives/decorators/bootstrap/radios.html +++ b/src/directives/decorators/bootstrap/radios.html @@ -14,5 +14,5 @@ -
+
diff --git a/src/directives/decorators/bootstrap/select.html b/src/directives/decorators/bootstrap/select.html index dbefbd208..68feabdf3 100644 --- a/src/directives/decorators/bootstrap/select.html +++ b/src/directives/decorators/bootstrap/select.html @@ -12,5 +12,5 @@ ng-options="item.value as item.name group by item.group for item in form.titleMap track by item[form.trackBy]" name="{{form.key.slice(-1)[0]}}"> -
+
diff --git a/src/directives/decorators/bootstrap/textarea.html b/src/directives/decorators/bootstrap/textarea.html index 06364edfc..64813acb4 100644 --- a/src/directives/decorators/bootstrap/textarea.html +++ b/src/directives/decorators/bootstrap/textarea.html @@ -31,5 +31,5 @@ ng-bind-html="form.fieldAddonRight"> - +