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","<div class=\"btn-group schema-form-actions {{form.htmlClass}}\" ng-transclude=\"\"></div>"); -$templateCache.put("directives/decorators/bootstrap/actions.html","<div class=\"btn-group schema-form-actions {{form.htmlClass}}\"><input ng-repeat-start=\"item in form.items\" type=\"submit\" class=\"btn {{ item.style || \'btn-default\' }} {{form.fieldHtmlClass}}\" value=\"{{item.title}}\" ng-if=\"item.type === \'submit\'\"> <button ng-repeat-end=\"\" class=\"btn {{ item.style || \'btn-default\' }} {{form.fieldHtmlClass}}\" type=\"button\" ng-disabled=\"form.readonly\" ng-if=\"item.type !== \'submit\'\" ng-click=\"buttonClick($event,item)\"><span ng-if=\"item.icon\" class=\"{{item.icon}}\"></span>{{item.title}}</button></div>"); -$templateCache.put("directives/decorators/bootstrap/array.html","<div sf-array=\"form\" class=\"schema-form-array {{form.htmlClass}}\" ng-model=\"$$value$$\" ng-model-options=\"form.ngModelOptions\"><h3 ng-show=\"form.title && form.notitle !== true\">{{ form.title }}</h3><ol class=\"list-group\" ng-model=\"modelArray\" ui-sortable=\"\"><li class=\"list-group-item {{form.fieldHtmlClass}}\" ng-repeat=\"item in modelArray track by $index\"><button ng-hide=\"form.readonly || form.remove === null\" ng-click=\"deleteFromArray($index)\" style=\"position: relative; z-index: 20;\" type=\"button\" class=\"close pull-right\"><span aria-hidden=\"true\">×</span><span class=\"sr-only\">Close</span></button><sf-decorator ng-init=\"arrayIndex = $index\" form=\"copyWithIndex($index)\"></sf-decorator></li></ol><div class=\"clearfix\" style=\"padding: 15px;\"><button ng-hide=\"form.readonly || form.add === null\" ng-click=\"appendToArray()\" type=\"button\" class=\"btn {{ form.style.add || \'btn-default\' }} pull-right\"><i class=\"glyphicon glyphicon-plus\"></i> {{ form.add || \'Add\'}}</button></div><div class=\"help-block\" ng-show=\"(hasError() && errorMessage(schemaError())) || form.description\" ng-bind-html=\"(hasError() && errorMessage(schemaError())) || form.description\"></div></div>"); -$templateCache.put("directives/decorators/bootstrap/checkbox.html","<div class=\"checkbox schema-form-checkbox {{form.htmlClass}}\" ng-class=\"{\'has-error\': form.disableErrorState !== true && hasError(), \'has-success\': form.disableSuccessState !== true && hasSuccess()}\"><label class=\"{{form.labelHtmlClass}}\"><input type=\"checkbox\" sf-changed=\"form\" ng-disabled=\"form.readonly\" ng-model=\"$$value$$\" ng-model-options=\"form.ngModelOptions\" schema-validate=\"form\" class=\"{{form.fieldHtmlClass}}\" name=\"{{form.key.slice(-1)[0]}}\"> <span ng-bind-html=\"form.title\"></span></label><div class=\"help-block\" sf-message=\"form.description\"></div></div>"); -$templateCache.put("directives/decorators/bootstrap/checkboxes.html","<div sf-array=\"form\" ng-model=\"$$value$$\" class=\"form-group schema-form-checkboxes {{form.htmlClass}}\" ng-class=\"{\'has-error\': form.disableErrorState !== true && hasError(), \'has-success\': form.disableSuccessState !== true && hasSuccess()}\"><label class=\"control-label {{form.labelHtmlClass}}\" ng-show=\"showTitle()\">{{form.title}}</label><div class=\"checkbox\" ng-repeat=\"val in titleMapValues track by $index\"><label><input type=\"checkbox\" ng-disabled=\"form.readonly\" sf-changed=\"form\" class=\"{{form.fieldHtmlClass}}\" ng-model=\"titleMapValues[$index]\" name=\"{{form.key.slice(-1)[0]}}\"> <span ng-bind-html=\"form.titleMap[$index].name\"></span></label></div><div class=\"help-block\" sf-message=\"form.description\"></div></div>"); -$templateCache.put("directives/decorators/bootstrap/default.html","<div class=\"form-group schema-form-{{form.type}} {{form.htmlClass}}\" ng-class=\"{\'has-error\': form.disableErrorState !== true && hasError(), \'has-success\': form.disableSuccessState !== true && hasSuccess(), \'has-feedback\': form.feedback !== false }\"><label class=\"control-label {{form.labelHtmlClass}}\" ng-class=\"{\'sr-only\': !showTitle()}\" for=\"{{form.key.slice(-1)[0]}}\">{{form.title}}</label> <input ng-if=\"!form.fieldAddonLeft && !form.fieldAddonRight\" ng-show=\"form.key\" type=\"{{form.type}}\" step=\"any\" sf-changed=\"form\" placeholder=\"{{form.placeholder}}\" class=\"form-control {{form.fieldHtmlClass}}\" id=\"{{form.key.slice(-1)[0]}}\" ng-model-options=\"form.ngModelOptions\" ng-model=\"$$value$$\" ng-disabled=\"form.readonly\" schema-validate=\"form\" name=\"{{form.key.slice(-1)[0]}}\" aria-describedby=\"{{form.key.slice(-1)[0] + \'Status\'}}\"><div ng-if=\"form.fieldAddonLeft || form.fieldAddonRight\" ng-class=\"{\'input-group\': (form.fieldAddonLeft || form.fieldAddonRight)}\"><span ng-if=\"form.fieldAddonLeft\" class=\"input-group-addon\" ng-bind-html=\"form.fieldAddonLeft\"></span> <input ng-show=\"form.key\" type=\"{{form.type}}\" step=\"any\" sf-changed=\"form\" placeholder=\"{{form.placeholder}}\" class=\"form-control {{form.fieldHtmlClass}}\" id=\"{{form.key.slice(-1)[0]}}\" ng-model-options=\"form.ngModelOptions\" ng-model=\"$$value$$\" ng-disabled=\"form.readonly\" schema-validate=\"form\" name=\"{{form.key.slice(-1)[0]}}\" aria-describedby=\"{{form.key.slice(-1)[0] + \'Status\'}}\"> <span ng-if=\"form.fieldAddonRight\" class=\"input-group-addon\" ng-bind-html=\"form.fieldAddonRight\"></span></div><span ng-if=\"form.feedback !== false\" class=\"form-control-feedback\" ng-class=\"evalInScope(form.feedback) || {\'glyphicon\': true, \'glyphicon-ok\': hasSuccess(), \'glyphicon-remove\': hasError() }\" aria-hidden=\"true\"></span> <span ng-if=\"hasError() || hasSuccess()\" id=\"{{form.key.slice(-1)[0] + \'Status\'}}\" class=\"sr-only\">{{ hasSuccess() ? \'(success)\' : \'(error)\' }}</span><div class=\"help-block\" sf-message=\"form.description\"></div></div>"); -$templateCache.put("directives/decorators/bootstrap/fieldset-trcl.html","<fieldset ng-disabled=\"form.readonly\" class=\"schema-form-fieldset {{form.htmlClass}}\"><legend ng-show=\"form.title\">{{ form.title }}</legend><div class=\"help-block\" ng-show=\"form.description\" ng-bind-html=\"form.description\"></div><div ng-transclude=\"\"></div></fieldset>"); -$templateCache.put("directives/decorators/bootstrap/fieldset.html","<fieldset ng-disabled=\"form.readonly\" class=\"schema-form-fieldset {{form.htmlClass}}\"><legend ng-show=\"form.title\">{{ form.title }}</legend><div class=\"help-block\" ng-show=\"form.description\" ng-bind-html=\"form.description\"></div><sf-decorator ng-repeat=\"item in form.items\" form=\"item\"></sf-decorator></fieldset>"); -$templateCache.put("directives/decorators/bootstrap/help.html","<div class=\"helpvalue schema-form-helpvalue {{form.htmlClass}}\" ng-bind-html=\"form.helpvalue\"></div>"); -$templateCache.put("directives/decorators/bootstrap/radio-buttons.html","<div class=\"form-group schema-form-radiobuttons {{form.htmlClass}}\" ng-class=\"{\'has-error\': form.disableErrorState !== true && hasError(), \'has-success\': form.disableSuccessState !== true && hasSuccess()}\"><div><label class=\"control-label {{form.labelHtmlClass}}\" ng-show=\"showTitle()\">{{form.title}}</label></div><div class=\"btn-group\"><label class=\"btn {{ (item.value === $$value$$) ? form.style.selected || \'btn-default\' : form.style.unselected || \'btn-default\'; }}\" ng-class=\"{ active: item.value === $$value$$ }\" ng-repeat=\"item in form.titleMap\"><input type=\"radio\" class=\"{{form.fieldHtmlClass}}\" sf-changed=\"form\" style=\"display: none;\" ng-disabled=\"form.readonly\" ng-model=\"$$value$$\" ng-model-options=\"form.ngModelOptions\" schema-validate=\"form\" ng-value=\"item.value\" name=\"{{form.key.join(\'.\')}}\"> <span ng-bind-html=\"item.name\"></span></label></div><div class=\"help-block\" sf-message=\"form.description\"></div></div>"); -$templateCache.put("directives/decorators/bootstrap/radios-inline.html","<div class=\"form-group schema-form-radios-inline {{form.htmlClass}}\" ng-class=\"{\'has-error\': form.disableErrorState !== true && hasError(), \'has-success\': form.disableSuccessState !== true && hasSuccess()}\"><label class=\"control-label {{form.labelHtmlClass}}\" ng-show=\"showTitle()\">{{form.title}}</label><div><label class=\"radio-inline\" ng-repeat=\"item in form.titleMap\"><input type=\"radio\" class=\"{{form.fieldHtmlClass}}\" sf-changed=\"form\" ng-disabled=\"form.readonly\" ng-model=\"$$value$$\" schema-validate=\"form\" ng-value=\"item.value\" name=\"{{form.key.join(\'.\')}}\"> <span ng-bind-html=\"item.name\"></span></label></div><div class=\"help-block\" sf-message=\"form.description\"></div></div>"); -$templateCache.put("directives/decorators/bootstrap/radios.html","<div class=\"form-group schema-form-radios {{form.htmlClass}}\" ng-class=\"{\'has-error\': form.disableErrorState !== true && hasError(), \'has-success\': form.disableSuccessState !== true && hasSuccess()}\"><label class=\"control-label {{form.labelHtmlClass}}\" ng-show=\"showTitle()\">{{form.title}}</label><div class=\"radio\" ng-repeat=\"item in form.titleMap\"><label><input type=\"radio\" class=\"{{form.fieldHtmlClass}}\" sf-changed=\"form\" ng-disabled=\"form.readonly\" ng-model=\"$$value$$\" ng-model-options=\"form.ngModelOptions\" schema-validate=\"form\" ng-value=\"item.value\" name=\"{{form.key.join(\'.\')}}\"> <span ng-bind-html=\"item.name\"></span></label></div><div class=\"help-block\" sf-message=\"form.description\"></div></div>"); -$templateCache.put("directives/decorators/bootstrap/section.html","<div class=\"schema-form-section {{form.htmlClass}}\"><sf-decorator ng-repeat=\"item in form.items\" form=\"item\"></sf-decorator></div>"); -$templateCache.put("directives/decorators/bootstrap/select.html","<div class=\"form-group {{form.htmlClass}} schema-form-select\" ng-class=\"{\'has-error\': form.disableErrorState !== true && hasError(), \'has-success\': form.disableSuccessState !== true && hasSuccess(), \'has-feedback\': form.feedback !== false}\"><label class=\"control-label {{form.labelHtmlClass}}\" ng-show=\"showTitle()\">{{form.title}}</label><select ng-model=\"$$value$$\" ng-model-options=\"form.ngModelOptions\" ng-disabled=\"form.readonly\" sf-changed=\"form\" class=\"form-control {{form.fieldHtmlClass}}\" schema-validate=\"form\" ng-options=\"item.value as item.name group by item.group for item in form.titleMap\" name=\"{{form.key.slice(-1)[0]}}\"></select><div class=\"help-block\" sf-message=\"form.description\"></div></div>"); -$templateCache.put("directives/decorators/bootstrap/submit.html","<div class=\"form-group schema-form-submit {{form.htmlClass}}\"><input type=\"submit\" class=\"btn {{ form.style || \'btn-primary\' }} {{form.fieldHtmlClass}}\" value=\"{{form.title}}\" ng-disabled=\"form.readonly\" ng-if=\"form.type === \'submit\'\"> <button class=\"btn {{ form.style || \'btn-default\' }}\" type=\"button\" ng-click=\"buttonClick($event,form)\" ng-disabled=\"form.readonly\" ng-if=\"form.type !== \'submit\'\"><span ng-if=\"form.icon\" class=\"{{form.icon}}\"></span> {{form.title}}</button></div>"); -$templateCache.put("directives/decorators/bootstrap/tabarray.html","<div sf-array=\"form\" ng-init=\"selected = { tab: 0 }\" class=\"clearfix schema-form-tabarray schema-form-tabarray-{{form.tabType || \'left\'}} {{form.htmlClass}}\"><div ng-if=\"!form.tabType || form.tabType !== \'right\'\" ng-class=\"{\'col-xs-3\': !form.tabType || form.tabType === \'left\'}\"><ul class=\"nav nav-tabs\" ng-class=\"{ \'tabs-left\': !form.tabType || form.tabType === \'left\'}\"><li ng-repeat=\"item in modelArray track by $index\" ng-click=\"$event.preventDefault() || (selected.tab = $index)\" ng-class=\"{active: selected.tab === $index}\"><a href=\"#\">{{interp(form.title,{\'$index\':$index, value: item}) || $index}}</a></li><li ng-hide=\"form.readonly\" ng-click=\"$event.preventDefault() || (selected.tab = appendToArray().length - 1)\"><a href=\"#\"><i class=\"glyphicon glyphicon-plus\"></i> {{ form.add || \'Add\'}}</a></li></ul></div><div ng-class=\"{\'col-xs-9\': !form.tabType || form.tabType === \'left\' || form.tabType === \'right\'}\"><div class=\"tab-content {{form.fieldHtmlClass}}\"><div class=\"tab-pane clearfix\" ng-repeat=\"item in modelArray track by $index\" ng-show=\"selected.tab === $index\" ng-class=\"{active: selected.tab === $index}\"><sf-decorator ng-init=\"arrayIndex = $index\" form=\"copyWithIndex($index)\"></sf-decorator><button ng-hide=\"form.readonly\" ng-click=\"selected.tab = deleteFromArray($index).length - 1\" type=\"button\" class=\"btn {{ form.style.remove || \'btn-default\' }} pull-right\"><i class=\"glyphicon glyphicon-trash\"></i> {{ form.remove || \'Remove\'}}</button></div></div></div><div ng-if=\"form.tabType === \'right\'\" class=\"col-xs-3\"><ul class=\"nav nav-tabs tabs-right\"><li ng-repeat=\"item in modelArray track by $index\" ng-click=\"$event.preventDefault() || (selected.tab = $index)\" ng-class=\"{active: selected.tab === $index}\"><a href=\"#\">{{interp(form.title,{\'$index\':$index, value: item}) || $index}}</a></li><li ng-hide=\"form.readonly\" ng-click=\"$event.preventDefault() || appendToArray()\"><a href=\"#\"><i class=\"glyphicon glyphicon-plus\"></i> {{ form.add || \'Add\'}}</a></li></ul></div></div>"); -$templateCache.put("directives/decorators/bootstrap/tabs.html","<div ng-init=\"selected = { tab: 0 }\" class=\"schema-form-tabs {{form.htmlClass}}\"><ul class=\"nav nav-tabs\"><li ng-repeat=\"tab in form.tabs\" ng-disabled=\"form.readonly\" ng-click=\"$event.preventDefault() || (selected.tab = $index)\" ng-class=\"{active: selected.tab === $index}\"><a href=\"#\">{{ tab.title }}</a></li></ul><div class=\"tab-content {{form.fieldHtmlClass}}\"><div class=\"tab-pane\" ng-disabled=\"form.readonly\" ng-repeat=\"tab in form.tabs\" ng-show=\"selected.tab === $index\" ng-class=\"{active: selected.tab === $index}\"><bootstrap-decorator ng-repeat=\"item in tab.items\" form=\"item\"></bootstrap-decorator></div></div></div>"); -$templateCache.put("directives/decorators/bootstrap/textarea.html","<div class=\"form-group has-feedback {{form.htmlClass}} schema-form-textarea\" ng-class=\"{\'has-error\': form.disableErrorState !== true && hasError(), \'has-success\': form.disableSuccessState !== true && hasSuccess()}\"><label class=\"{{form.labelHtmlClass}}\" ng-class=\"{\'sr-only\': !showTitle()}\" for=\"{{form.key.slice(-1)[0]}}\">{{form.title}}</label> <textarea ng-if=\"!form.fieldAddonLeft && !form.fieldAddonRight\" class=\"form-control {{form.fieldHtmlClass}}\" id=\"{{form.key.slice(-1)[0]}}\" sf-changed=\"form\" placeholder=\"{{form.placeholder}}\" ng-disabled=\"form.readonly\" ng-model=\"$$value$$\" ng-model-options=\"form.ngModelOptions\" schema-validate=\"form\" name=\"{{form.key.slice(-1)[0]}}\"></textarea><div ng-if=\"form.fieldAddonLeft || form.fieldAddonRight\" ng-class=\"{\'input-group\': (form.fieldAddonLeft || form.fieldAddonRight)}\"><span ng-if=\"form.fieldAddonLeft\" class=\"input-group-addon\" ng-bind-html=\"form.fieldAddonLeft\"></span> <textarea class=\"form-control {{form.fieldHtmlClass}}\" id=\"{{form.key.slice(-1)[0]}}\" sf-changed=\"form\" placeholder=\"{{form.placeholder}}\" ng-disabled=\"form.readonly\" ng-model=\"$$value$$\" ng-model-options=\"form.ngModelOptions\" schema-validate=\"form\" name=\"{{form.key.slice(-1)[0]}}\"></textarea> <span ng-if=\"form.fieldAddonRight\" class=\"input-group-addon\" ng-bind-html=\"form.fieldAddonRight\"></span></div><span class=\"help-block\" sf-message=\"form.description\"></span></div>");}]); -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",'<div class="btn-group schema-form-actions {{form.htmlClass}}" ng-transclude=""></div>'),e.put("directives/decorators/bootstrap/actions.html",'<div class="btn-group schema-form-actions {{form.htmlClass}}"><input ng-repeat-start="item in form.items" type="submit" class="btn {{ item.style || \'btn-default\' }} {{form.fieldHtmlClass}}" value="{{item.title}}" ng-if="item.type === \'submit\'"> <button ng-repeat-end="" class="btn {{ item.style || \'btn-default\' }} {{form.fieldHtmlClass}}" type="button" ng-disabled="form.readonly" ng-if="item.type !== \'submit\'" ng-click="buttonClick($event,item)"><span ng-if="item.icon" class="{{item.icon}}"></span>{{item.title}}</button></div>'),e.put("directives/decorators/bootstrap/array.html",'<div sf-array="form" class="schema-form-array {{form.htmlClass}}" ng-model="$$value$$" ng-model-options="form.ngModelOptions"><h3 ng-show="form.title && form.notitle !== true">{{ form.title }}</h3><ol class="list-group" ng-model="modelArray" ui-sortable=""><li class="list-group-item {{form.fieldHtmlClass}}" ng-repeat="item in modelArray track by $index"><button ng-hide="form.readonly || form.remove === null" ng-click="deleteFromArray($index)" style="position: relative; z-index: 20;" type="button" class="close pull-right"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button><sf-decorator ng-init="arrayIndex = $index" form="copyWithIndex($index)"></sf-decorator></li></ol><div class="clearfix" style="padding: 15px;"><button ng-hide="form.readonly || form.add === null" ng-click="appendToArray()" type="button" class="btn {{ form.style.add || \'btn-default\' }} pull-right"><i class="glyphicon glyphicon-plus"></i> {{ form.add || \'Add\'}}</button></div><div class="help-block" ng-show="(hasError() && errorMessage(schemaError())) || form.description" ng-bind-html="(hasError() && errorMessage(schemaError())) || form.description"></div></div>'),e.put("directives/decorators/bootstrap/checkbox.html",'<div class="checkbox schema-form-checkbox {{form.htmlClass}}" ng-class="{\'has-error\': form.disableErrorState !== true && hasError(), \'has-success\': form.disableSuccessState !== true && hasSuccess()}"><label class="{{form.labelHtmlClass}}"><input type="checkbox" sf-changed="form" ng-disabled="form.readonly" ng-model="$$value$$" ng-model-options="form.ngModelOptions" schema-validate="form" class="{{form.fieldHtmlClass}}" name="{{form.key.slice(-1)[0]}}"> <span ng-bind-html="form.title"></span></label><div class="help-block" sf-message="form.description"></div></div>'),e.put("directives/decorators/bootstrap/checkboxes.html",'<div sf-array="form" ng-model="$$value$$" class="form-group schema-form-checkboxes {{form.htmlClass}}" ng-class="{\'has-error\': form.disableErrorState !== true && hasError(), \'has-success\': form.disableSuccessState !== true && hasSuccess()}"><label class="control-label {{form.labelHtmlClass}}" ng-show="showTitle()">{{form.title}}</label><div class="checkbox" ng-repeat="val in titleMapValues track by $index"><label><input type="checkbox" ng-disabled="form.readonly" sf-changed="form" class="{{form.fieldHtmlClass}}" ng-model="titleMapValues[$index]" name="{{form.key.slice(-1)[0]}}"> <span ng-bind-html="form.titleMap[$index].name"></span></label></div><div class="help-block" sf-message="form.description"></div></div>'),e.put("directives/decorators/bootstrap/default.html",'<div class="form-group schema-form-{{form.type}} {{form.htmlClass}}" ng-class="{\'has-error\': form.disableErrorState !== true && hasError(), \'has-success\': form.disableSuccessState !== true && hasSuccess(), \'has-feedback\': form.feedback !== false }"><label class="control-label {{form.labelHtmlClass}}" ng-class="{\'sr-only\': !showTitle()}" for="{{form.key.slice(-1)[0]}}">{{form.title}}</label> <input ng-if="!form.fieldAddonLeft && !form.fieldAddonRight" ng-show="form.key" type="{{form.type}}" step="any" sf-changed="form" placeholder="{{form.placeholder}}" class="form-control {{form.fieldHtmlClass}}" id="{{form.key.slice(-1)[0]}}" ng-model-options="form.ngModelOptions" ng-model="$$value$$" ng-disabled="form.readonly" schema-validate="form" name="{{form.key.slice(-1)[0]}}" aria-describedby="{{form.key.slice(-1)[0] + \'Status\'}}"><div ng-if="form.fieldAddonLeft || form.fieldAddonRight" ng-class="{\'input-group\': (form.fieldAddonLeft || form.fieldAddonRight)}"><span ng-if="form.fieldAddonLeft" class="input-group-addon" ng-bind-html="form.fieldAddonLeft"></span> <input ng-show="form.key" type="{{form.type}}" step="any" sf-changed="form" placeholder="{{form.placeholder}}" class="form-control {{form.fieldHtmlClass}}" id="{{form.key.slice(-1)[0]}}" ng-model-options="form.ngModelOptions" ng-model="$$value$$" ng-disabled="form.readonly" schema-validate="form" name="{{form.key.slice(-1)[0]}}" aria-describedby="{{form.key.slice(-1)[0] + \'Status\'}}"> <span ng-if="form.fieldAddonRight" class="input-group-addon" ng-bind-html="form.fieldAddonRight"></span></div><span ng-if="form.feedback !== false" class="form-control-feedback" ng-class="evalInScope(form.feedback) || {\'glyphicon\': true, \'glyphicon-ok\': hasSuccess(), \'glyphicon-remove\': hasError() }" aria-hidden="true"></span> <span ng-if="hasError() || hasSuccess()" id="{{form.key.slice(-1)[0] + \'Status\'}}" class="sr-only">{{ hasSuccess() ? \'(success)\' : \'(error)\' }}</span><div class="help-block" sf-message="form.description"></div></div>'),e.put("directives/decorators/bootstrap/fieldset-trcl.html",'<fieldset ng-disabled="form.readonly" class="schema-form-fieldset {{form.htmlClass}}"><legend ng-show="form.title">{{ form.title }}</legend><div class="help-block" ng-show="form.description" ng-bind-html="form.description"></div><div ng-transclude=""></div></fieldset>'),e.put("directives/decorators/bootstrap/fieldset.html",'<fieldset ng-disabled="form.readonly" class="schema-form-fieldset {{form.htmlClass}}"><legend ng-show="form.title">{{ form.title }}</legend><div class="help-block" ng-show="form.description" ng-bind-html="form.description"></div><sf-decorator ng-repeat="item in form.items" form="item"></sf-decorator></fieldset>'),e.put("directives/decorators/bootstrap/help.html",'<div class="helpvalue schema-form-helpvalue {{form.htmlClass}}" ng-bind-html="form.helpvalue"></div>'),e.put("directives/decorators/bootstrap/radio-buttons.html",'<div class="form-group schema-form-radiobuttons {{form.htmlClass}}" ng-class="{\'has-error\': form.disableErrorState !== true && hasError(), \'has-success\': form.disableSuccessState !== true && hasSuccess()}"><div><label class="control-label {{form.labelHtmlClass}}" ng-show="showTitle()">{{form.title}}</label></div><div class="btn-group"><label class="btn {{ (item.value === $$value$$) ? form.style.selected || \'btn-default\' : form.style.unselected || \'btn-default\'; }}" ng-class="{ active: item.value === $$value$$ }" ng-repeat="item in form.titleMap"><input type="radio" class="{{form.fieldHtmlClass}}" sf-changed="form" style="display: none;" ng-disabled="form.readonly" ng-model="$$value$$" ng-model-options="form.ngModelOptions" schema-validate="form" ng-value="item.value" name="{{form.key.join(\'.\')}}"> <span ng-bind-html="item.name"></span></label></div><div class="help-block" sf-message="form.description"></div></div>'),e.put("directives/decorators/bootstrap/radios-inline.html",'<div class="form-group schema-form-radios-inline {{form.htmlClass}}" ng-class="{\'has-error\': form.disableErrorState !== true && hasError(), \'has-success\': form.disableSuccessState !== true && hasSuccess()}"><label class="control-label {{form.labelHtmlClass}}" ng-show="showTitle()">{{form.title}}</label><div><label class="radio-inline" ng-repeat="item in form.titleMap"><input type="radio" class="{{form.fieldHtmlClass}}" sf-changed="form" ng-disabled="form.readonly" ng-model="$$value$$" schema-validate="form" ng-value="item.value" name="{{form.key.join(\'.\')}}"> <span ng-bind-html="item.name"></span></label></div><div class="help-block" sf-message="form.description"></div></div>'),e.put("directives/decorators/bootstrap/radios.html",'<div class="form-group schema-form-radios {{form.htmlClass}}" ng-class="{\'has-error\': form.disableErrorState !== true && hasError(), \'has-success\': form.disableSuccessState !== true && hasSuccess()}"><label class="control-label {{form.labelHtmlClass}}" ng-show="showTitle()">{{form.title}}</label><div class="radio" ng-repeat="item in form.titleMap"><label><input type="radio" class="{{form.fieldHtmlClass}}" sf-changed="form" ng-disabled="form.readonly" ng-model="$$value$$" ng-model-options="form.ngModelOptions" schema-validate="form" ng-value="item.value" name="{{form.key.join(\'.\')}}"> <span ng-bind-html="item.name"></span></label></div><div class="help-block" sf-message="form.description"></div></div>'),e.put("directives/decorators/bootstrap/section.html",'<div class="schema-form-section {{form.htmlClass}}"><sf-decorator ng-repeat="item in form.items" form="item"></sf-decorator></div>'),e.put("directives/decorators/bootstrap/select.html",'<div class="form-group {{form.htmlClass}} schema-form-select" ng-class="{\'has-error\': form.disableErrorState !== true && hasError(), \'has-success\': form.disableSuccessState !== true && hasSuccess(), \'has-feedback\': form.feedback !== false}"><label class="control-label {{form.labelHtmlClass}}" ng-show="showTitle()">{{form.title}}</label><select ng-model="$$value$$" ng-model-options="form.ngModelOptions" ng-disabled="form.readonly" sf-changed="form" class="form-control {{form.fieldHtmlClass}}" schema-validate="form" ng-options="item.value as item.name group by item.group for item in form.titleMap" name="{{form.key.slice(-1)[0]}}"></select><div class="help-block" sf-message="form.description"></div></div>'),e.put("directives/decorators/bootstrap/submit.html",'<div class="form-group schema-form-submit {{form.htmlClass}}"><input type="submit" class="btn {{ form.style || \'btn-primary\' }} {{form.fieldHtmlClass}}" value="{{form.title}}" ng-disabled="form.readonly" ng-if="form.type === \'submit\'"> <button class="btn {{ form.style || \'btn-default\' }}" type="button" ng-click="buttonClick($event,form)" ng-disabled="form.readonly" ng-if="form.type !== \'submit\'"><span ng-if="form.icon" class="{{form.icon}}"></span> {{form.title}}</button></div>'),e.put("directives/decorators/bootstrap/tabarray.html",'<div sf-array="form" ng-init="selected = { tab: 0 }" class="clearfix schema-form-tabarray schema-form-tabarray-{{form.tabType || \'left\'}} {{form.htmlClass}}"><div ng-if="!form.tabType || form.tabType !== \'right\'" ng-class="{\'col-xs-3\': !form.tabType || form.tabType === \'left\'}"><ul class="nav nav-tabs" ng-class="{ \'tabs-left\': !form.tabType || form.tabType === \'left\'}"><li ng-repeat="item in modelArray track by $index" ng-click="$event.preventDefault() || (selected.tab = $index)" ng-class="{active: selected.tab === $index}"><a href="#">{{interp(form.title,{\'$index\':$index, value: item}) || $index}}</a></li><li ng-hide="form.readonly" ng-click="$event.preventDefault() || (selected.tab = appendToArray().length - 1)"><a href="#"><i class="glyphicon glyphicon-plus"></i> {{ form.add || \'Add\'}}</a></li></ul></div><div ng-class="{\'col-xs-9\': !form.tabType || form.tabType === \'left\' || form.tabType === \'right\'}"><div class="tab-content {{form.fieldHtmlClass}}"><div class="tab-pane clearfix" ng-repeat="item in modelArray track by $index" ng-show="selected.tab === $index" ng-class="{active: selected.tab === $index}"><sf-decorator ng-init="arrayIndex = $index" form="copyWithIndex($index)"></sf-decorator><button ng-hide="form.readonly" ng-click="selected.tab = deleteFromArray($index).length - 1" type="button" class="btn {{ form.style.remove || \'btn-default\' }} pull-right"><i class="glyphicon glyphicon-trash"></i> {{ form.remove || \'Remove\'}}</button></div></div></div><div ng-if="form.tabType === \'right\'" class="col-xs-3"><ul class="nav nav-tabs tabs-right"><li ng-repeat="item in modelArray track by $index" ng-click="$event.preventDefault() || (selected.tab = $index)" ng-class="{active: selected.tab === $index}"><a href="#">{{interp(form.title,{\'$index\':$index, value: item}) || $index}}</a></li><li ng-hide="form.readonly" ng-click="$event.preventDefault() || appendToArray()"><a href="#"><i class="glyphicon glyphicon-plus"></i> {{ form.add || \'Add\'}}</a></li></ul></div></div>'),e.put("directives/decorators/bootstrap/tabs.html",'<div ng-init="selected = { tab: 0 }" class="schema-form-tabs {{form.htmlClass}}"><ul class="nav nav-tabs"><li ng-repeat="tab in form.tabs" ng-disabled="form.readonly" ng-click="$event.preventDefault() || (selected.tab = $index)" ng-class="{active: selected.tab === $index}"><a href="#">{{ tab.title }}</a></li></ul><div class="tab-content {{form.fieldHtmlClass}}"><div class="tab-pane" ng-disabled="form.readonly" ng-repeat="tab in form.tabs" ng-show="selected.tab === $index" ng-class="{active: selected.tab === $index}"><bootstrap-decorator ng-repeat="item in tab.items" form="item"></bootstrap-decorator></div></div></div>'),e.put("directives/decorators/bootstrap/textarea.html",'<div class="form-group has-feedback {{form.htmlClass}} schema-form-textarea" ng-class="{\'has-error\': form.disableErrorState !== true && hasError(), \'has-success\': form.disableSuccessState !== true && hasSuccess()}"><label class="{{form.labelHtmlClass}}" ng-class="{\'sr-only\': !showTitle()}" for="{{form.key.slice(-1)[0]}}">{{form.title}}</label> <textarea ng-if="!form.fieldAddonLeft && !form.fieldAddonRight" class="form-control {{form.fieldHtmlClass}}" id="{{form.key.slice(-1)[0]}}" sf-changed="form" placeholder="{{form.placeholder}}" ng-disabled="form.readonly" ng-model="$$value$$" ng-model-options="form.ngModelOptions" schema-validate="form" name="{{form.key.slice(-1)[0]}}"></textarea><div ng-if="form.fieldAddonLeft || form.fieldAddonRight" ng-class="{\'input-group\': (form.fieldAddonLeft || form.fieldAddonRight)}"><span ng-if="form.fieldAddonLeft" class="input-group-addon" ng-bind-html="form.fieldAddonLeft"></span> <textarea class="form-control {{form.fieldHtmlClass}}" id="{{form.key.slice(-1)[0]}}" sf-changed="form" placeholder="{{form.placeholder}}" ng-disabled="form.readonly" ng-model="$$value$$" ng-model-options="form.ngModelOptions" schema-validate="form" name="{{form.key.slice(-1)[0]}}"></textarea> <span ng-if="form.fieldAddonRight" class="input-group-addon" ng-bind-html="form.fieldAddonRight"></span></div><span class="help-block" sf-message="form.description"></span></div>')}]),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: '<sf-decorator form="form"></sf-decorator>', - 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 <schema-form> directive in which case the decorator is - * given it's configuration via a the "form" attribute. - * - * ex. Basic usage - * <sf-decorator form="myform"></sf-decorator> - ** - * @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 <schema-form> directive in which case the decorator is - * given it's configuration via a the "form" attribute. - * - * ex. Basic usage - * <sf-decorator form="myform"></sf-decorator> - ** - * @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 <schema-form> directive. - * Specifically when you need to reuse styling. - * - * ex. createDirective('text','...') - * <sf-text title="foobar" model="person" key="name" schema="schema"></sf-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 -<form sf-form="form" sf-schema="schema" sf-decorator="foobar"></form> -*/ - -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;l<o.length;l++){if(""===o[l])return void 0;if("undefined"!=typeof i){if(l===o.length-1)return a[o[l]]=i,i;var s=a[o[l]];("undefined"==typeof s||null===s)&&(s=t.test(o[l+1])?[]:{},a[o[l]]=s),a=s}else a&&(a=a[o[l]])}return a}}]),e.module("schemaForm").factory("sfBuilder",["$templateCache","schemaFormDecorators","sfPath",function(e,t,r){var n=/[A-Z]/g,i=function(e,t){return t=t||"_",e.replace(n,function(e,r){return(r?t:"")+e.toLowerCase()})},o=function(e,t){if(e.key){var n=t[r.stringify(e.key)];if(n){for(;n.firstChild;)n.removeChild(n.firstChild);return n}}},a=function(e,t,n,l,s){s=s||"schemaForm.form";var c=document.createDocumentFragment();return e.reduce(function(e,c,u){if(c.type){var f=t[c.type]||t["default"];if(f.replace){var m,d=document.createElement("div"),p=n(f.template)||n([t["default"].template]);if(c.key){var h=c.key?r.stringify(c.key).replace(/"/g,"""):"";p=p.replace(/\$\$value\$\$/g,"model"+("["!==h[0]?".":"")+h)}for(d.innerHTML=p,m=document.createDocumentFragment();d.childNodes.length>0;)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:'<sf-decorator form="form"></sf-decorator>',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;l<o.length;l++)if(a=o[l](t,n,i))return a.schema["x-schema-form"]&&e.isObject(a.schema["x-schema-form"])&&(a=e.extend(a,a.schema["x-schema-form"])),a},a=function(t,r,n){n=n||{};var o=n.global&&n.global.formDefaults?e.copy(n.global.formDefaults):{};return n.global&&n.global.supressPropertyTitles===!0?o.title=r.title:o.title=r.title||t,r.description&&(o.description=r.description),(n.required===!0||r.required===!0)&&(o.required=!0),r.maxLength&&(o.maxlength=r.maxLength),r.minLength&&(o.minlength=r.maxLength),(r.readOnly||r.readonly)&&(o.readonly=!0),r.minimum&&(o.minimum=r.minimum+(r.exclusiveMinimum?1:0)),r.maximum&&(o.maximum=r.maximum-(r.exclusiveMaximum?1:0)),r.validationMessage&&(o.validationMessage=r.validationMessage),r.enumNames&&(o.titleMap=i(r.enumNames,r["enum"])),o.schema=r,o.ngModelOptions=o.ngModelOptions||{},o},l=function(e,n,i){if("string"===r(n.type)&&!n["enum"]){var o=a(e,n,i);return o.key=i.path,o.type="text",i.lookup[t.stringify(i.path)]=o,o}},s=function(e,n,i){if("number"===r(n.type)){var o=a(e,n,i);return o.key=i.path,o.type="number",i.lookup[t.stringify(i.path)]=o,o}},c=function(e,n,i){if("integer"===r(n.type)){var o=a(e,n,i);return o.key=i.path,o.type="number",i.lookup[t.stringify(i.path)]=o,o}},u=function(e,n,i){if("boolean"===r(n.type)){var o=a(e,n,i);return o.key=i.path,o.type="checkbox",i.lookup[t.stringify(i.path)]=o,o}},f=function(e,i,o){if("string"===r(i.type)&&i["enum"]){var l=a(e,i,o);return l.key=o.path,l.type="select",l.titleMap||(l.titleMap=n(i["enum"])),l.trackBy="value",o.lookup[t.stringify(o.path)]=l,l}},m=function(e,i,o){if("array"===r(i.type)&&i.items&&i.items["enum"]){var l=a(e,i,o);return l.key=o.path,l.type="checkboxes",l.titleMap||(l.titleMap=n(i.items["enum"])),o.lookup[t.stringify(o.path)]=l,l}},d=function(n,i,l){if("object"===r(i.type)){var s=a(n,i,l);return s.type="fieldset",s.items=[],l.lookup[t.stringify(l.path)]=s,e.forEach(i.properties,function(e,r){var n=l.path.slice();if(n.push(r),l.ignore[t.stringify(n)]!==!0){var a=i.required&&-1!==i.required.indexOf(r),c=o(r,e,{path:n,required:a||!1,lookup:l.lookup,ignore:l.ignore,global:l.global});c&&s.items.push(c)}}),s}},p=function(e,n,i){if("array"===r(n.type)){var l=a(e,n,i);l.type="array",l.key=i.path,i.lookup[t.stringify(i.path)]=l;var s=n.required&&-1!==n.required.indexOf(i.path[i.path.length-1]),c=i.path.slice();return c.push(""),l.items=[o(e,n.items,{path:c,required:s||!1,lookup:i.lookup,ignore:i.ignore,global:i.global})],l}},h={string:[f,l],object:[d],number:[s],integer:[c],"boolean":[u],array:[m,p]},v=function(e){return e};this.defaults=h,this.stdFormObj=a,this.defaultFormDefinition=o,this.postProcess=function(e){v=e},this.appendRule=function(e,t){h[e]||(h[e]=[]),h[e].push(t)},this.prependRule=function(e,t){h[e]||(h[e]=[]),h[e].unshift(t)},this.createStandardForm=a,this.$get=function(){var n={};return n.merge=function(r,o,a,l,s){o=o||["*"],l=l||{},s=s||r.readonly||r.readOnly;var c=n.defaults(r,a,l),u=o.indexOf("*");-1!==u&&(o=o.slice(0,u).concat(c.form).concat(o.slice(u+1)));var f=c.lookup;return v(o.map(function(o){if("string"==typeof o&&(o={key:o}),o.key&&"string"==typeof o.key&&(o.key=t.parse(o.key)),o.titleMap&&(o.titleMap=i(o.titleMap)),"select"===o.type&&(o.trackBy=o.trackBy||"value"),o.itemForm){o.items=[];var c=t.stringify(o.key),u=f[c];e.forEach(u.items,function(t){var r=e.copy(o.itemForm);r.key=t.key,o.items.push(r)})}if(o.key){var m=t.stringify(o.key);if(f[m]){var d=f[m];e.forEach(d,function(e,t){void 0===o[t]&&(o[t]=d[t])})}}return s===!0&&(o.readonly=!0),o.items&&(o.items=n.merge(r,o.items,a,l,o.readonly)),o.tabs&&e.forEach(o.tabs,function(e){e.items=n.merge(r,e.items,a,l,o.readonly)}),"checkbox"===o.type&&e.isUndefined(o.schema["default"])&&(o.schema["default"]=!1),o}))},n.defaults=function(t,n,i){var a=[],l={};if(n=n||{},i=i||{},"object"!==r(t.type))throw new Error('Not implemented. Only type "object" allowed at root level of schema.');return e.forEach(t.properties,function(e,r){if(n[r]!==!0){var s=t.required&&-1!==t.required.indexOf(r),c=o(r,e,{path:[r],lookup:l,ignore:n,required:s,global:i});c&&a.push(c)}}),{form:a,lookup:l}},n.traverseSchema=function(t,r,n,i){i=e.isDefined(i)?i:!0,n=n||[];var o=function(t,r,n){if(r(t,n),e.forEach(t.properties,function(e,t){var i=n.slice();i.push(t),o(e,r,i)}),!i&&t.items){var a=n.slice();a.push(""),o(t.items,r,a)}};o(t,r,n||[])},n.traverseForm=function(t,r){r(t),e.forEach(t.items,function(e){n.traverseForm(e,r)}),t.tabs&&e.forEach(t.tabs,function(t){e.forEach(t.items,function(e){n.traverseForm(e,r)})})},n}}]),e.module("schemaForm").factory("sfValidator",[function(){var t={};return t.validate=function(t,n){if(!t)return{valid:!0};var i=t.schema;if(!i)return{valid:!0};""===n&&(n=void 0),"number"===t.type&&null===n&&(n=void 0);var o={type:"object",properties:{}},a=t.key[t.key.length-1];o.properties[a]=i,t.required&&(o.required=[a]);var l={};return e.isDefined(n)&&(l[a]=n),r.validateResult(l,o)},t}]),e.module("schemaForm").directive("sfArray",["sfSelect","schemaForm","sfValidator","sfPath",function(t,r,n,i){var o=function(e){return function(t){t.key&&(t.key[t.key.indexOf("")]=e)}};return{restrict:"A",scope:!0,require:"?ngModel",link:function(a,l,s,c){var u={};a.validateArray=e.noop,c&&a.$emit("schemaFormPropagateNgModelController",c);var f=a.$watch(s.sfArray,function(l){var s=t(l.key,a.model);if(a.$watch("model"+i.normalize(l.key),function(e){s=a.modelArray=e}),e.isUndefined(s)&&(s=[],t(l.key,a.model,s)),a.modelArray=s,l.items){var m=l.items[0];l.items.length>1&&(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;r<t.length;r++){var n=t[r].getAttribute("ng-model");f[n.substring(n.indexOf(".")+1)]=!0}}});var m,d={},p=function(c,u){var d=r.merge(c,u,f,o.options);m&&(o.externalDestructionInProgress=!0,m.$destroy(),o.externalDestructionInProgress=!1),m=o.$new(),m.schemaForm={form:d,schema:c},l.children(":not(.schema-form-ignore)").remove();for(var p={},h=l[0].querySelectorAll("*[sf-insert-field]"),v=0;v<h.length;v++)p[h[v].getAttribute("sf-insert-field")]=h[v];var y=n.decorator(s.sfUseDecorator);l[0].appendChild(a.build(d,y,p)),t(l.children())(m),o.options&&o.options.setSchemaDefaults===!1||r.traverseSchema(c,function(t,r){if(e.isDefined(t["default"])){var n=i(r,o.model);e.isUndefined(n)&&i(r,o.model,t["default"])}}),o.$emit("sf-render-finished",l)};o.$watch(function(){var e=o.schema,t=o.initialForm||["*"];t&&e&&e.type&&(d.form!==t||d.schema!==e)&&Object.keys(e.properties).length>0&&(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 @@ <h3 ng-show="form.title && form.notitle !== true">{{ form.title }}</h3> {{ form.add || 'Add'}} </button> </div> - <div class="help-block" + <div class="help-block {{form.validationHtmlClass}}" ng-show="(hasError() && errorMessage(schemaError())) || form.description" ng-bind-html="(hasError() && errorMessage(schemaError())) || form.description"></div> </div> 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]}}"> <span ng-bind-html="form.title"></span> </label> - <div class="help-block" sf-message="form.description"></div> + <div class="help-block {{form.validationHtmlClass}}" sf-message="form.description"></div> </div> 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 @@ </label> </div> - <div class="help-block" sf-message="form.description"></div> + <div class="help-block {{form.validationHtmlClass}}" sf-message="form.description"></div> </div> 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)' }}</span> - <div class="help-block" sf-message="form.description"></div> + <div class="help-block {{form.validationHtmlClass}}" sf-message="form.description"></div> </div> 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 @@ <fieldset ng-disabled="form.readonly" class="schema-form-fieldset {{form.htmlClass}}"> <legend ng-class="{'sr-only': !showTitle() }">{{ form.title }}</legend> - <div class="help-block" ng-show="form.description" ng-bind-html="form.description"></div> + <div class="help-block {{form.validationHtmlClass}}" ng-show="form.description" ng-bind-html="form.description"></div> <div ng-transclude></div> </fieldset> 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 @@ <fieldset ng-disabled="form.readonly" class="schema-form-fieldset {{form.htmlClass}}"> <legend ng-class="{'sr-only': !showTitle() }">{{ form.title }}</legend> - <div class="help-block" ng-show="form.description" ng-bind-html="form.description"></div> + <div class="help-block {{form.validationHtmlClass}}" ng-show="form.description" ng-bind-html="form.description"></div> <sf-decorator ng-repeat="item in form.items" form="item"></sf-decorator> </fieldset> 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 @@ <span ng-bind-html="item.name"></span> </label> </div> - <div class="help-block" sf-message="form.description"></div> + <div class="help-block {{form.validationHtmlClass}}" sf-message="form.description"></div> </div> 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 @@ <span ng-bind-html="item.name"></span> </label> </div> - <div class="help-block" sf-message="form.description"></div> + <div class="help-block {{form.validationHtmlClass}}" sf-message="form.description"></div> </div> 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 @@ <span ng-bind-html="item.name"></span> </label> </div> - <div class="help-block" sf-message="form.description"></div> + <div class="help-block {{form.validationHtmlClass}}" sf-message="form.description"></div> </div> 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]}}"> </select> - <div class="help-block" sf-message="form.description"></div> + <div class="help-block {{form.validationHtmlClass}}" sf-message="form.description"></div> </div> 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"></span> </div> - <span class="help-block" sf-message="form.description"></span> + <span class="help-block {{form.validationHtmlClass}}" sf-message="form.description"></span> </div>