├── .editorconfig
├── .gitignore
├── .travis.yml
├── Gruntfile.coffee
├── LICENSE
├── README.md
├── components
└── default-components.coffee
├── dist
├── angular-form-builder-components.js
├── angular-form-builder-components.min.js
├── angular-form-builder.css
├── angular-form-builder.js
└── angular-form-builder.min.js
├── example
├── demo.coffee
├── demo.js
├── popoverTemplate.html
├── site.css
├── site.scss
└── template.html
├── index.html
├── package.json
├── src
├── angular-form-builder.scss
├── controller.coffee
├── directive.coffee
├── drag.coffee
├── module.coffee
└── provider.coffee
└── test
├── karma.config.coffee
└── specs
├── controllerSpec.coffee
├── directiveSpec.coffee
└── providerSpec.coffee
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Unix-style newlines with a newline ending every file
7 | [*]
8 | end_of_line = lf
9 | insert_final_newline = true
10 |
11 | # 4 space indentation
12 | [*.{coffee,html}]
13 | indent_style = space
14 | indent_size = 4
15 |
16 | # 2 space indentation
17 | [*.{yml,json}]
18 | indent_style = space
19 | indent_size = 2
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OS X noise
2 | .DS_Store
3 |
4 | # PyCharm
5 | .idea
6 |
7 | # node
8 | node_modules
9 | package-lock.json
10 |
11 | # compass
12 | .sass-cache
13 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 8.9.4
4 |
5 | install:
6 | - export DISPLAY=:99.0
7 | - sh -e /etc/init.d/xvfb start
8 | - npm install -g grunt-cli
9 | - npm install
10 |
11 | script:
12 | - npm test
13 |
--------------------------------------------------------------------------------
/Gruntfile.coffee:
--------------------------------------------------------------------------------
1 | module.exports = (grunt) ->
2 | grunt.config.init
3 | compass:
4 | example:
5 | options:
6 | sassDir: 'example'
7 | cssDir: 'example'
8 | outputStyle: 'compressed'
9 | src:
10 | options:
11 | sassDir: 'src'
12 | cssDir: 'dist'
13 | outputStyle: 'compressed'
14 |
15 | coffee:
16 | source:
17 | files:
18 | 'dist/angular-form-builder.js': ['src/*.coffee']
19 | components:
20 | files:
21 | 'dist/angular-form-builder-components.js': ['components/*.coffee']
22 | demo:
23 | files:
24 | 'example/demo.js': 'example/demo.coffee'
25 |
26 | uglify:
27 | build:
28 | files:
29 | 'dist/angular-form-builder.min.js': 'dist/angular-form-builder.js'
30 | 'dist/angular-form-builder-components.min.js': 'dist/angular-form-builder-components.js'
31 |
32 | watch:
33 | compass:
34 | files: ['example/*.scss', 'src/*.scss']
35 | tasks: ['compass']
36 | options:
37 | spawn: no
38 | coffee:
39 | files: ['src/*.coffee', 'components/*.coffee', 'example/*.coffee']
40 | tasks: ['coffee']
41 | options:
42 | spawn: no
43 |
44 | connect:
45 | server:
46 | options:
47 | protocol: 'http'
48 | hostname: '*'
49 | port: 8000
50 | base: '.'
51 |
52 | karma:
53 | test:
54 | configFile: 'test/karma.config.coffee'
55 |
56 | # -----------------------------------
57 | # register task
58 | # -----------------------------------
59 | grunt.registerTask 'dev', [
60 | 'compass'
61 | 'coffee'
62 | 'connect'
63 | 'watch'
64 | ]
65 | grunt.registerTask 'build', [
66 | 'compass'
67 | 'coffee'
68 | 'uglify'
69 | ]
70 |
71 | # -----------------------------------
72 | # Plugins
73 | # -----------------------------------
74 | grunt.loadNpmTasks 'grunt-contrib-compass'
75 | grunt.loadNpmTasks 'grunt-contrib-coffee'
76 | grunt.loadNpmTasks 'grunt-contrib-watch'
77 | grunt.loadNpmTasks 'grunt-contrib-connect'
78 | grunt.loadNpmTasks 'grunt-karma'
79 | grunt.loadNpmTasks 'grunt-contrib-uglify'
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Kelp
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # angular-form-builder [](http://travis-ci.org/kelp404/angular-form-builder) [](https://david-dm.org/kelp404/angular-form-builder#info=devDependencies&view=table)
2 |
3 | [MIT License](http://www.opensource.org/licenses/mit-license.php)
4 |
5 |
6 | This is an AngularJS form builder written in [CoffeeScript](http://coffeescript.org).
7 |
8 |
9 |
10 |
11 | ## Frameworks
12 | 1. [AngularJS](http://angularjs.org/) 1.2.32
13 | 2. [jQuery](http://jquery.com/) 3.3.1
14 | 3. [Bootstrap 3](http://getbootstrap.com/)
15 | 4. [angular-validator](https://github.com/kelp404/angular-validator)
16 |
17 |
18 |
19 |
20 | ## $builder
21 | ```coffee
22 | # just $builder
23 | angular.module 'yourApp', ['builder']
24 |
25 | # include $builder and default components
26 | angular.module 'yourApp', ['builder', 'builder.components']
27 | ```
28 |
29 |
30 | #### components
31 | ```coffee
32 | ###
33 | All components.
34 | ###
35 | $builder.components =
36 | componentName{string}: component{object}
37 | ```
38 |
39 |
40 | #### groups
41 | ```coffee
42 | ###
43 | All groups of components.
44 | ###
45 | $builder.groups = [componentGroup{string}]
46 | ```
47 |
48 |
49 | #### registerComponent
50 | ```coffee
51 | # .config
52 | $builderProvider.registerComponent = (name, component={}) ->
53 | ###
54 | Register the component for form-builder.
55 | @param name: The component name.
56 | @param component: The component object.
57 | group: {string} The component group.
58 | label: {string} The label of the input.
59 | description: {string} The description of the input.
60 | placeholder: {string} The placeholder of the input.
61 | editable: {bool} Is the form object editable?
62 | required: {bool} Is the form object required?
63 | validation: {string} angular-validator. "/regex/" or "[rule1, rule2]". (default is '/.*/')
64 | validationOptions: {array} [{rule: angular-validator, label: 'option label'}] the options for the validation. (default is [])
65 | options: {array} The input options.
66 | arrayToText: {bool} checkbox could use this to convert input (default is no)
67 | template: {string} html template
68 | templateUrl: {string} The url of the template.
69 | popoverTemplate: {string} html template
70 | popoverTemplateUrl: {string} The url of the popover template.
71 | ###
72 | # .run
73 | $builder.registerComponent = (name, component={}) ->
74 | ```
75 |
76 | **component.template**
77 | ```html
78 |
85 | ```
86 |
87 | **component.popoverTemplate**
88 | ```html
89 |
118 | ```
119 |
120 |
121 | #### forms
122 | ```coffee
123 | ###
124 | builder mode: `fb-builder` you could drag and drop to build the form.
125 | form mode: `fb-form` this is the form for end-user to input value.
126 | Default is {default: []}
127 | ###
128 | $builder.forms =
129 | formName{string}: formObjects{array}
130 | ```
131 |
132 |
133 | #### insertFormObject
134 | ```coffee
135 | $builder.insertFormObject = (name, index, formObject={}) =>
136 | ###
137 | Insert the form object into the form at {index}.
138 | @param name: The form name.
139 | @param index: The form object index.
140 | @param form: The form object.
141 | id: The form object id.
142 | component: {string} The component name
143 | editable: {bool} Is the form object editable? (default is yes)
144 | label: {string} The form object label.
145 | description: {string} The form object description.
146 | placeholder: {string} The form object placeholder.
147 | options: {array} The form object options.
148 | required: {bool} Is the form object required? (default is no)
149 | validation: {string} angular-validator. "/regex/" or "[rule1, rule2]".
150 | [index]: {int} The form object index. It will be generated by $builder.
151 | @return: The form object.
152 | ###
153 | ```
154 |
155 | #### addFormObject
156 | ```coffee
157 | $builder.addFormObject = (name, formObject={}) =>
158 | ###
159 | Insert the form object into the form at last.
160 | reference $builder.insertFormObject()
161 | ###
162 | ```
163 |
164 | #### removeFormObject
165 | ```coffee
166 | $builder.removeFormObject = (name, index) =>
167 | ###
168 | Remove the form object by the index.
169 | @param name: {string} The form name.
170 | @param index: {int} The form object index.
171 | ###
172 | ```
173 |
174 |
175 |
176 |
177 | ## builder.directive
178 | #### fb-components
179 | ```coffee
180 | a = angular.module 'builder.directive', ['builder.provider', 'builder.controller', 'builder.drag', 'validator']
181 | fbComponents = ->
182 | ###
183 | You could use `fb-components` to render the components view.
184 | ###
185 | restrict: 'A'
186 | template:
187 | """
188 |
193 |
197 | """
198 | controller: 'fbComponentsController'
199 | a.directive 'fbComponents', fbComponents
200 | ```
201 |
202 | ```html
203 |
204 | ```
205 |
206 |
207 | #### fb-builder
208 | ```coffee
209 | a = angular.module 'builder.directive', ['builder.provider', 'builder.controller', 'builder.drag', 'validator']
210 | fbBuilder = ($injector) ->
211 | ###
212 | You could use `fb-builder="formName"` to render the builder view.
213 | ###
214 | restrict: 'A'
215 | template:
216 | """
217 |
221 | """
222 | link: (scope, element, attrs) ->
223 | fbBuilder.$inject = ['$injector']
224 | a.directive 'fbBuilder', fbBuilder
225 | ```
226 |
227 | ```html
228 |
229 | ```
230 |
231 |
232 | #### fb-form
233 | ```coffee
234 | a = angular.module 'builder.directive', ['builder.provider', 'builder.controller', 'builder.drag', 'validator']
235 | fbForm = ($injector) ->
236 | ###
237 | You could use `fb-form="formName"` to render the form view for end-users.
238 | ###
239 | restrict: 'A'
240 | require: 'ngModel' # form data (end-user input value)
241 | scope:
242 | # input model for scops in ng-repeat
243 | input: '=ngModel'
244 | template:
245 | """
246 |
247 |
248 | """
249 | controller: 'fbFormController'
250 | link: (scope, element, attrs) ->
251 | fbForm.$inject = ['$injector']
252 | a.directive 'fbForm', fbForm
253 | ```
254 |
255 | ```html
256 |
264 | ```
265 |
266 |
267 |
268 |
269 | ## builder.components
270 | > There are some default components at this module. This module is not required.
271 | + textInput
272 | + textArea
273 | + checkbox
274 | + radio
275 | + select
276 |
277 |
278 |
279 |
280 | ## Unit Test
281 | ```bash
282 | $ npm test
283 | ```
284 |
285 |
286 |
287 |
288 | ## Development
289 | ```bash
290 | # install node modules
291 | $ npm install
292 | # install bower components
293 | $ bower install
294 | ```
295 | ```bash
296 | # run the local server and the file watcher to compile CoffeeScript
297 | $ grunt dev
298 | # compile coffee script and minify
299 | $ grunt build
300 | ```
301 |
--------------------------------------------------------------------------------
/components/default-components.coffee:
--------------------------------------------------------------------------------
1 | angular.module 'builder.components', ['builder', 'validator.rules']
2 |
3 | .config ['$builderProvider', ($builderProvider) ->
4 | # ----------------------------------------
5 | # text input
6 | # ----------------------------------------
7 | $builderProvider.registerComponent 'textInput',
8 | group: 'Default'
9 | label: 'Text Input'
10 | description: 'description'
11 | placeholder: 'placeholder'
12 | required: no
13 | validationOptions: [
14 | {label: 'none', rule: '/.*/'}
15 | {label: 'number', rule: '[number]'}
16 | {label: 'email', rule: '[email]'}
17 | {label: 'url', rule: '[url]'}
18 | ]
19 | template:
20 | """
21 |
28 | """
29 | popoverTemplate:
30 | """
31 |
61 | """
62 |
63 | # ----------------------------------------
64 | # Text area
65 | # ----------------------------------------
66 | $builderProvider.registerComponent 'textArea',
67 | group: 'Default'
68 | label: 'Text Area'
69 | description: 'description'
70 | placeholder: 'placeholder'
71 | required: no
72 | template:
73 | """
74 |
81 | """
82 | popoverTemplate:
83 | """
84 |
110 | """
111 |
112 | # ----------------------------------------
113 | # checkbox
114 | # ----------------------------------------
115 | $builderProvider.registerComponent 'checkbox',
116 | group: 'Default'
117 | label: 'Checkbox'
118 | description: 'description'
119 | placeholder: 'placeholder'
120 | required: no
121 | options: ['value one', 'value two']
122 | arrayToText: yes
123 | template:
124 | """
125 |
137 | """
138 | popoverTemplate:
139 | """
140 |
167 | """
168 |
169 | # ----------------------------------------
170 | # radio
171 | # ----------------------------------------
172 | $builderProvider.registerComponent 'radio',
173 | group: 'Default'
174 | label: 'Radio'
175 | description: 'description'
176 | placeholder: 'placeholder'
177 | required: no
178 | options: ['value one', 'value two']
179 | template:
180 | """
181 |
192 | """
193 | popoverTemplate:
194 | """
195 |
216 | """
217 |
218 | # ----------------------------------------
219 | # select
220 | # ----------------------------------------
221 | $builderProvider.registerComponent 'select',
222 | group: 'Default'
223 | label: 'Select'
224 | description: 'description'
225 | placeholder: 'placeholder'
226 | required: no
227 | options: ['value one', 'value two']
228 | template:
229 | """
230 |
238 | """
239 | popoverTemplate:
240 | """
241 |
262 | """
263 | ]
264 |
--------------------------------------------------------------------------------
/dist/angular-form-builder-components.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | angular.module('builder.components', ['builder', 'validator.rules']).config([
3 | '$builderProvider', function($builderProvider) {
4 | $builderProvider.registerComponent('textInput', {
5 | group: 'Default',
6 | label: 'Text Input',
7 | description: 'description',
8 | placeholder: 'placeholder',
9 | required: false,
10 | validationOptions: [
11 | {
12 | label: 'none',
13 | rule: '/.*/'
14 | }, {
15 | label: 'number',
16 | rule: '[number]'
17 | }, {
18 | label: 'email',
19 | rule: '[email]'
20 | }, {
21 | label: 'url',
22 | rule: '[url]'
23 | }
24 | ],
25 | template: "",
26 | popoverTemplate: ""
27 | });
28 | $builderProvider.registerComponent('textArea', {
29 | group: 'Default',
30 | label: 'Text Area',
31 | description: 'description',
32 | placeholder: 'placeholder',
33 | required: false,
34 | template: "",
35 | popoverTemplate: ""
36 | });
37 | $builderProvider.registerComponent('checkbox', {
38 | group: 'Default',
39 | label: 'Checkbox',
40 | description: 'description',
41 | placeholder: 'placeholder',
42 | required: false,
43 | options: ['value one', 'value two'],
44 | arrayToText: true,
45 | template: "",
46 | popoverTemplate: ""
47 | });
48 | $builderProvider.registerComponent('radio', {
49 | group: 'Default',
50 | label: 'Radio',
51 | description: 'description',
52 | placeholder: 'placeholder',
53 | required: false,
54 | options: ['value one', 'value two'],
55 | template: "",
56 | popoverTemplate: ""
57 | });
58 | return $builderProvider.registerComponent('select', {
59 | group: 'Default',
60 | label: 'Select',
61 | description: 'description',
62 | placeholder: 'placeholder',
63 | required: false,
64 | options: ['value one', 'value two'],
65 | template: "",
66 | popoverTemplate: ""
67 | });
68 | }
69 | ]);
70 |
71 | }).call(this);
72 |
--------------------------------------------------------------------------------
/dist/angular-form-builder-components.min.js:
--------------------------------------------------------------------------------
1 | (function(){angular.module("builder.components",["builder","validator.rules"]).config(["$builderProvider",function(a){return a.registerComponent("textInput",{group:"Default",label:"Text Input",description:"description",placeholder:"placeholder",required:!1,validationOptions:[{label:"none",rule:"/.*/"},{label:"number",rule:"[number]"},{label:"email",rule:"[email]"},{label:"url",rule:"[url]"}],template:'',popoverTemplate:""}),a.registerComponent("textArea",{group:"Default",label:"Text Area",description:"description",placeholder:"placeholder",required:!1,template:'',popoverTemplate:""}),a.registerComponent("checkbox",{group:"Default",label:"Checkbox",description:"description",placeholder:"placeholder",required:!1,options:["value one","value two"],arrayToText:!0,template:'',popoverTemplate:""}),a.registerComponent("radio",{group:"Default",label:"Radio",description:"description",placeholder:"placeholder",required:!1,options:["value one","value two"],template:'',popoverTemplate:""}),a.registerComponent("select",{group:"Default",label:"Select",description:"description",placeholder:"placeholder",required:!1,options:["value one","value two"],template:'',popoverTemplate:""})}])}).call(this);
--------------------------------------------------------------------------------
/dist/angular-form-builder.css:
--------------------------------------------------------------------------------
1 | .fb-component{padding:10px;cursor:move}.fb-component input{cursor:move}.fb-component label{cursor:move}.fb-component select{cursor:move}.fb-component textarea{cursor:move}.fb-form-object-editable{padding:10px}.fb-form-object-editable.fb-draggable{cursor:move}.fb-form-object-editable.fb-draggable input{cursor:move}.fb-form-object-editable.fb-draggable label{cursor:pointer}.fb-form-object-editable.fb-draggable select{cursor:move}.fb-form-object-editable.fb-draggable textarea{cursor:move}.fb-form-object-editable.empty{cursor:default;margin:6px;height:80px;border:dashed 1px #aaa;background-color:#eee}.fb-draggable.dragging{background-color:#ffffff;position:absolute;z-index:800;-webkit-box-shadow:#666 0 0 20px;-moz-box-shadow:#666 0 0 20px;box-shadow:#666 0 0 20px}.fb-required:after{color:#b94a48;content:' *'}.fb-builder{min-height:250px}.popover .control-label{text-align:left}.popover form{width:240px}
2 |
--------------------------------------------------------------------------------
/dist/angular-form-builder.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | var copyObjectToScope;
3 |
4 | copyObjectToScope = function(object, scope) {
5 |
6 | /*
7 | Copy object (ng-repeat="object in objects") to scope without `hashKey`.
8 | */
9 | var key, value;
10 | for (key in object) {
11 | value = object[key];
12 | if (key !== '$$hashKey') {
13 | scope[key] = value;
14 | }
15 | }
16 | };
17 |
18 | angular.module('builder.controller', ['builder.provider']).controller('fbFormObjectEditableController', [
19 | '$scope', '$injector', function($scope, $injector) {
20 | var $builder;
21 | $builder = $injector.get('$builder');
22 | $scope.setupScope = function(formObject) {
23 |
24 | /*
25 | 1. Copy origin formObject (ng-repeat="object in formObjects") to scope.
26 | 2. Setup optionsText with formObject.options.
27 | 3. Watch scope.label, .description, .placeholder, .required, .options then copy to origin formObject.
28 | 4. Watch scope.optionsText then convert to scope.options.
29 | 5. setup validationOptions
30 | */
31 | var component;
32 | copyObjectToScope(formObject, $scope);
33 | $scope.optionsText = formObject.options.join('\n');
34 | $scope.$watch('[label, description, placeholder, required, options, validation]', function() {
35 | formObject.label = $scope.label;
36 | formObject.description = $scope.description;
37 | formObject.placeholder = $scope.placeholder;
38 | formObject.required = $scope.required;
39 | formObject.options = $scope.options;
40 | return formObject.validation = $scope.validation;
41 | }, true);
42 | $scope.$watch('optionsText', function(text) {
43 | var x;
44 | $scope.options = (function() {
45 | var _i, _len, _ref, _results;
46 | _ref = text.split('\n');
47 | _results = [];
48 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
49 | x = _ref[_i];
50 | if (x.length > 0) {
51 | _results.push(x);
52 | }
53 | }
54 | return _results;
55 | })();
56 | return $scope.inputText = $scope.options[0];
57 | });
58 | component = $builder.components[formObject.component];
59 | return $scope.validationOptions = component.validationOptions;
60 | };
61 | return $scope.data = {
62 | model: null,
63 | backup: function() {
64 |
65 | /*
66 | Backup input value.
67 | */
68 | return this.model = {
69 | label: $scope.label,
70 | description: $scope.description,
71 | placeholder: $scope.placeholder,
72 | required: $scope.required,
73 | optionsText: $scope.optionsText,
74 | validation: $scope.validation
75 | };
76 | },
77 | rollback: function() {
78 |
79 | /*
80 | Rollback input value.
81 | */
82 | if (!this.model) {
83 | return;
84 | }
85 | $scope.label = this.model.label;
86 | $scope.description = this.model.description;
87 | $scope.placeholder = this.model.placeholder;
88 | $scope.required = this.model.required;
89 | $scope.optionsText = this.model.optionsText;
90 | return $scope.validation = this.model.validation;
91 | }
92 | };
93 | }
94 | ]).controller('fbComponentsController', [
95 | '$scope', '$injector', function($scope, $injector) {
96 | var $builder;
97 | $builder = $injector.get('$builder');
98 | $scope.selectGroup = function($event, group) {
99 | var component, name, _ref, _results;
100 | if ($event != null) {
101 | $event.preventDefault();
102 | }
103 | $scope.activeGroup = group;
104 | $scope.components = [];
105 | _ref = $builder.components;
106 | _results = [];
107 | for (name in _ref) {
108 | component = _ref[name];
109 | if (component.group === group) {
110 | _results.push($scope.components.push(component));
111 | }
112 | }
113 | return _results;
114 | };
115 | $scope.groups = $builder.groups;
116 | $scope.activeGroup = $scope.groups[0];
117 | $scope.allComponents = $builder.components;
118 | return $scope.$watch('allComponents', function() {
119 | return $scope.selectGroup(null, $scope.activeGroup);
120 | });
121 | }
122 | ]).controller('fbComponentController', [
123 | '$scope', function($scope) {
124 | return $scope.copyObjectToScope = function(object) {
125 | return copyObjectToScope(object, $scope);
126 | };
127 | }
128 | ]).controller('fbFormController', [
129 | '$scope', '$injector', function($scope, $injector) {
130 | var $builder, $timeout;
131 | $builder = $injector.get('$builder');
132 | $timeout = $injector.get('$timeout');
133 | if ($scope.input == null) {
134 | $scope.input = [];
135 | }
136 | return $scope.$watch('form', function() {
137 | if ($scope.input.length > $scope.form.length) {
138 | $scope.input.splice($scope.form.length);
139 | }
140 | return $timeout(function() {
141 | return $scope.$broadcast($builder.broadcastChannel.updateInput);
142 | });
143 | }, true);
144 | }
145 | ]).controller('fbFormObjectController', [
146 | '$scope', '$injector', function($scope, $injector) {
147 | var $builder;
148 | $builder = $injector.get('$builder');
149 | $scope.copyObjectToScope = function(object) {
150 | return copyObjectToScope(object, $scope);
151 | };
152 | return $scope.updateInput = function(value) {
153 |
154 | /*
155 | Copy current scope.input[X] to $parent.input.
156 | @param value: The input value.
157 | */
158 | var input;
159 | input = {
160 | id: $scope.formObject.id,
161 | label: $scope.formObject.label,
162 | value: value != null ? value : ''
163 | };
164 | return $scope.$parent.input.splice($scope.$index, 1, input);
165 | };
166 | }
167 | ]);
168 |
169 | }).call(this);
170 |
171 | (function() {
172 | angular.module('builder.directive', ['builder.provider', 'builder.controller', 'builder.drag', 'validator']).directive('fbBuilder', [
173 | '$injector', function($injector) {
174 | var $builder, $drag;
175 | $builder = $injector.get('$builder');
176 | $drag = $injector.get('$drag');
177 | return {
178 | restrict: 'A',
179 | scope: {
180 | fbBuilder: '='
181 | },
182 | template: "",
183 | link: function(scope, element, attrs) {
184 | var beginMove, _base, _name;
185 | scope.formName = attrs.fbBuilder;
186 | if ((_base = $builder.forms)[_name = scope.formName] == null) {
187 | _base[_name] = [];
188 | }
189 | scope.formObjects = $builder.forms[scope.formName];
190 | beginMove = true;
191 | $(element).addClass('fb-builder');
192 | return $drag.droppable($(element), {
193 | move: function(e) {
194 | var $empty, $formObject, $formObjects, height, index, offset, positions, _i, _j, _ref, _ref1;
195 | if (beginMove) {
196 | $("div.fb-form-object-editable").popover('hide');
197 | beginMove = false;
198 | }
199 | $formObjects = $(element).find('.fb-form-object-editable:not(.empty,.dragging)');
200 | if ($formObjects.length === 0) {
201 | if ($(element).find('.fb-form-object-editable.empty').length === 0) {
202 | $(element).find('>div:first').append($(""));
203 | }
204 | return;
205 | }
206 | positions = [];
207 | positions.push(-1000);
208 | for (index = _i = 0, _ref = $formObjects.length; _i < _ref; index = _i += 1) {
209 | $formObject = $($formObjects[index]);
210 | offset = $formObject.offset();
211 | height = $formObject.height();
212 | positions.push(offset.top + height / 2);
213 | }
214 | positions.push(positions[positions.length - 1] + 1000);
215 | for (index = _j = 1, _ref1 = positions.length; _j < _ref1; index = _j += 1) {
216 | if (e.pageY > positions[index - 1] && e.pageY <= positions[index]) {
217 | $(element).find('.empty').remove();
218 | $empty = $("");
219 | if (index - 1 < $formObjects.length) {
220 | $empty.insertBefore($($formObjects[index - 1]));
221 | } else {
222 | $empty.insertAfter($($formObjects[index - 2]));
223 | }
224 | break;
225 | }
226 | }
227 | },
228 | out: function() {
229 | if (beginMove) {
230 | $("div.fb-form-object-editable").popover('hide');
231 | beginMove = false;
232 | }
233 | return $(element).find('.empty').remove();
234 | },
235 | up: function(e, isHover, draggable) {
236 | var formObject, newIndex, oldIndex;
237 | beginMove = true;
238 | if (!$drag.isMouseMoved()) {
239 | $(element).find('.empty').remove();
240 | return;
241 | }
242 | if (!isHover && draggable.mode === 'drag') {
243 | formObject = draggable.object.formObject;
244 | if (formObject.editable) {
245 | $builder.removeFormObject(attrs.fbBuilder, formObject.index);
246 | }
247 | } else if (isHover) {
248 | if (draggable.mode === 'mirror') {
249 | $builder.insertFormObject(scope.formName, $(element).find('.empty').index('.fb-form-object-editable'), {
250 | component: draggable.object.componentName
251 | });
252 | }
253 | if (draggable.mode === 'drag') {
254 | oldIndex = draggable.object.formObject.index;
255 | newIndex = $(element).find('.empty').index('.fb-form-object-editable');
256 | if (oldIndex < newIndex) {
257 | newIndex--;
258 | }
259 | $builder.updateFormObjectIndex(scope.formName, oldIndex, newIndex);
260 | }
261 | }
262 | return $(element).find('.empty').remove();
263 | }
264 | });
265 | }
266 | };
267 | }
268 | ]).directive('fbFormObjectEditable', [
269 | '$injector', function($injector) {
270 | var $builder, $compile, $drag, $validator;
271 | $builder = $injector.get('$builder');
272 | $drag = $injector.get('$drag');
273 | $compile = $injector.get('$compile');
274 | $validator = $injector.get('$validator');
275 | return {
276 | restrict: 'A',
277 | controller: 'fbFormObjectEditableController',
278 | scope: {
279 | formObject: '=fbFormObjectEditable'
280 | },
281 | link: function(scope, element) {
282 | var popover;
283 | scope.inputArray = [];
284 | scope.$component = $builder.components[scope.formObject.component];
285 | scope.setupScope(scope.formObject);
286 | scope.$watch('$component.template', function(template) {
287 | var view;
288 | if (!template) {
289 | return;
290 | }
291 | view = $compile(template)(scope);
292 | return $(element).html(view);
293 | });
294 | $(element).on('click', function() {
295 | return false;
296 | });
297 | $drag.draggable($(element), {
298 | object: {
299 | formObject: scope.formObject
300 | }
301 | });
302 | if (!scope.formObject.editable) {
303 | return;
304 | }
305 | popover = {};
306 | scope.$watch('$component.popoverTemplate', function(template) {
307 | if (!template) {
308 | return;
309 | }
310 | $(element).removeClass(popover.id);
311 | popover = {
312 | id: "fb-" + (Math.random().toString().substr(2)),
313 | isClickedSave: false,
314 | view: null,
315 | html: template
316 | };
317 | popover.html = $(popover.html).addClass(popover.id);
318 | popover.view = $compile(popover.html)(scope);
319 | $(element).addClass(popover.id);
320 | return $(element).popover({
321 | html: true,
322 | title: scope.$component.label,
323 | content: popover.view,
324 | container: 'body',
325 | placement: $builder.config.popoverPlacement
326 | });
327 | });
328 | scope.popover = {
329 | save: function($event) {
330 |
331 | /*
332 | The save event of the popover.
333 | */
334 | $event.preventDefault();
335 | $validator.validate(scope).success(function() {
336 | popover.isClickedSave = true;
337 | return $(element).popover('hide');
338 | });
339 | },
340 | remove: function($event) {
341 |
342 | /*
343 | The delete event of the popover.
344 | */
345 | $event.preventDefault();
346 | $builder.removeFormObject(scope.$parent.formName, scope.$parent.$index);
347 | $(element).popover('hide');
348 | },
349 | shown: function() {
350 |
351 | /*
352 | The shown event of the popover.
353 | */
354 | scope.data.backup();
355 | return popover.isClickedSave = false;
356 | },
357 | cancel: function($event) {
358 |
359 | /*
360 | The cancel event of the popover.
361 | */
362 | scope.data.rollback();
363 | if ($event) {
364 | $event.preventDefault();
365 | $(element).popover('hide');
366 | }
367 | }
368 | };
369 | $(element).on('show.bs.popover', function() {
370 | var $popover, elementOrigin, popoverTop;
371 | if ($drag.isMouseMoved()) {
372 | return false;
373 | }
374 | $("div.fb-form-object-editable:not(." + popover.id + ")").popover('hide');
375 | $popover = $("form." + popover.id).closest('.popover');
376 | if ($popover.length > 0) {
377 | elementOrigin = $(element).offset().top + $(element).height() / 2;
378 | popoverTop = elementOrigin - $popover.height() / 2;
379 | $popover.css({
380 | position: 'absolute',
381 | top: popoverTop
382 | });
383 | $popover.show();
384 | setTimeout(function() {
385 | $popover.addClass('in');
386 | return $(element).triggerHandler('shown.bs.popover');
387 | }, 0);
388 | return false;
389 | }
390 | });
391 | $(element).on('shown.bs.popover', function() {
392 | $(".popover ." + popover.id + " input:first").select();
393 | scope.$apply(function() {
394 | return scope.popover.shown();
395 | });
396 | });
397 | return $(element).on('hide.bs.popover', function() {
398 | var $popover;
399 | $popover = $("form." + popover.id).closest('.popover');
400 | if (!popover.isClickedSave) {
401 | if (scope.$$phase || scope.$root.$$phase) {
402 | scope.popover.cancel();
403 | } else {
404 | scope.$apply(function() {
405 | return scope.popover.cancel();
406 | });
407 | }
408 | }
409 | $popover.removeClass('in');
410 | setTimeout(function() {
411 | return $popover.hide();
412 | }, 300);
413 | return false;
414 | });
415 | }
416 | };
417 | }
418 | ]).directive('fbComponents', function() {
419 | return {
420 | restrict: 'A',
421 | template: " 1\" class=\"nav nav-tabs nav-justified\">\n - \n {{group}}\n
\n
\n",
422 | controller: 'fbComponentsController'
423 | };
424 | }).directive('fbComponent', [
425 | '$injector', function($injector) {
426 | var $builder, $compile, $drag;
427 | $builder = $injector.get('$builder');
428 | $drag = $injector.get('$drag');
429 | $compile = $injector.get('$compile');
430 | return {
431 | restrict: 'A',
432 | scope: {
433 | component: '=fbComponent'
434 | },
435 | controller: 'fbComponentController',
436 | link: function(scope, element) {
437 | scope.copyObjectToScope(scope.component);
438 | $drag.draggable($(element), {
439 | mode: 'mirror',
440 | defer: false,
441 | object: {
442 | componentName: scope.component.name
443 | }
444 | });
445 | return scope.$watch('component.template', function(template) {
446 | var view;
447 | if (!template) {
448 | return;
449 | }
450 | view = $compile(template)(scope);
451 | return $(element).html(view);
452 | });
453 | }
454 | };
455 | }
456 | ]).directive('fbForm', [
457 | '$injector', function($injector) {
458 | return {
459 | restrict: 'A',
460 | require: 'ngModel',
461 | scope: {
462 | formName: '@fbForm',
463 | input: '=ngModel',
464 | "default": '=fbDefault'
465 | },
466 | template: "",
467 | controller: 'fbFormController',
468 | link: function(scope, element, attrs) {
469 | var $builder, _base, _name;
470 | $builder = $injector.get('$builder');
471 | if ((_base = $builder.forms)[_name = scope.formName] == null) {
472 | _base[_name] = [];
473 | }
474 | return scope.form = $builder.forms[scope.formName];
475 | }
476 | };
477 | }
478 | ]).directive('fbFormObject', [
479 | '$injector', function($injector) {
480 | var $builder, $compile, $parse;
481 | $builder = $injector.get('$builder');
482 | $compile = $injector.get('$compile');
483 | $parse = $injector.get('$parse');
484 | return {
485 | restrict: 'A',
486 | controller: 'fbFormObjectController',
487 | link: function(scope, element, attrs) {
488 | scope.formObject = $parse(attrs.fbFormObject)(scope);
489 | scope.$component = $builder.components[scope.formObject.component];
490 | scope.$on($builder.broadcastChannel.updateInput, function() {
491 | return scope.updateInput(scope.inputText);
492 | });
493 | if (scope.$component.arrayToText) {
494 | scope.inputArray = [];
495 | scope.$watch('inputArray', function(newValue, oldValue) {
496 | var checked, index, _ref;
497 | if (newValue === oldValue) {
498 | return;
499 | }
500 | checked = [];
501 | for (index in scope.inputArray) {
502 | if (scope.inputArray[index]) {
503 | checked.push((_ref = scope.options[index]) != null ? _ref : scope.inputArray[index]);
504 | }
505 | }
506 | return scope.inputText = checked.join(', ');
507 | }, true);
508 | }
509 | scope.$watch('inputText', function() {
510 | return scope.updateInput(scope.inputText);
511 | });
512 | scope.$watch(attrs.fbFormObject, function() {
513 | return scope.copyObjectToScope(scope.formObject);
514 | }, true);
515 | scope.$watch('$component.template', function(template) {
516 | var $input, $template, view;
517 | if (!template) {
518 | return;
519 | }
520 | $template = $(template);
521 | $input = $template.find("[ng-model='inputText']");
522 | $input.attr({
523 | validator: '{{validation}}'
524 | });
525 | view = $compile($template)(scope);
526 | return $(element).html(view);
527 | });
528 | if (!scope.$component.arrayToText && scope.formObject.options.length > 0) {
529 | scope.inputText = scope.formObject.options[0];
530 | }
531 | return scope.$watch("default['" + scope.formObject.id + "']", function(value) {
532 | if (!value) {
533 | return;
534 | }
535 | if (scope.$component.arrayToText) {
536 | return scope.inputArray = value;
537 | } else {
538 | return scope.inputText = value;
539 | }
540 | });
541 | }
542 | };
543 | }
544 | ]);
545 |
546 | }).call(this);
547 |
548 | (function() {
549 | angular.module('builder.drag', []).provider('$drag', function() {
550 | var $injector, $rootScope, delay;
551 | $injector = null;
552 | $rootScope = null;
553 | this.data = {
554 | draggables: {},
555 | droppables: {}
556 | };
557 | this.mouseMoved = false;
558 | this.isMouseMoved = (function(_this) {
559 | return function() {
560 | return _this.mouseMoved;
561 | };
562 | })(this);
563 | this.hooks = {
564 | down: {},
565 | move: {},
566 | up: {}
567 | };
568 | this.eventMouseMove = function() {};
569 | this.eventMouseUp = function() {};
570 | $((function(_this) {
571 | return function() {
572 | $(document).on('mousedown', function(e) {
573 | var func, key, _ref;
574 | _this.mouseMoved = false;
575 | _ref = _this.hooks.down;
576 | for (key in _ref) {
577 | func = _ref[key];
578 | func(e);
579 | }
580 | });
581 | $(document).on('mousemove', function(e) {
582 | var func, key, _ref;
583 | _this.mouseMoved = true;
584 | _ref = _this.hooks.move;
585 | for (key in _ref) {
586 | func = _ref[key];
587 | func(e);
588 | }
589 | });
590 | return $(document).on('mouseup', function(e) {
591 | var func, key, _ref;
592 | _ref = _this.hooks.up;
593 | for (key in _ref) {
594 | func = _ref[key];
595 | func(e);
596 | }
597 | });
598 | };
599 | })(this));
600 | this.currentId = 0;
601 | this.getNewId = (function(_this) {
602 | return function() {
603 | return "" + (_this.currentId++);
604 | };
605 | })(this);
606 | this.setupEasing = function() {
607 | return jQuery.extend(jQuery.easing, {
608 | easeOutQuad: function(x, t, b, c, d) {
609 | return -c * (t /= d) * (t - 2) + b;
610 | }
611 | });
612 | };
613 | this.setupProviders = function(injector) {
614 |
615 | /*
616 | Setup providers.
617 | */
618 | $injector = injector;
619 | return $rootScope = $injector.get('$rootScope');
620 | };
621 | this.isHover = (function(_this) {
622 | return function($elementA, $elementB) {
623 |
624 | /*
625 | Is element A hover on element B?
626 | @param $elementA: jQuery object
627 | @param $elementB: jQuery object
628 | */
629 | var isHover, offsetA, offsetB, sizeA, sizeB;
630 | offsetA = $elementA.offset();
631 | offsetB = $elementB.offset();
632 | sizeA = {
633 | width: $elementA.width(),
634 | height: $elementA.height()
635 | };
636 | sizeB = {
637 | width: $elementB.width(),
638 | height: $elementB.height()
639 | };
640 | isHover = {
641 | x: false,
642 | y: false
643 | };
644 | isHover.x = offsetA.left > offsetB.left && offsetA.left < offsetB.left + sizeB.width;
645 | isHover.x = isHover.x || offsetA.left + sizeA.width > offsetB.left && offsetA.left + sizeA.width < offsetB.left + sizeB.width;
646 | if (!isHover) {
647 | return false;
648 | }
649 | isHover.y = offsetA.top > offsetB.top && offsetA.top < offsetB.top + sizeB.height;
650 | isHover.y = isHover.y || offsetA.top + sizeA.height > offsetB.top && offsetA.top + sizeA.height < offsetB.top + sizeB.height;
651 | return isHover.x && isHover.y;
652 | };
653 | })(this);
654 | delay = function(ms, func) {
655 | return setTimeout(function() {
656 | return func();
657 | }, ms);
658 | };
659 | this.autoScroll = {
660 | up: false,
661 | down: false,
662 | scrolling: false,
663 | scroll: (function(_this) {
664 | return function() {
665 | _this.autoScroll.scrolling = true;
666 | if (_this.autoScroll.up) {
667 | $('html, body').dequeue().animate({
668 | scrollTop: $(window).scrollTop() - 50
669 | }, 100, 'easeOutQuad');
670 | return delay(100, function() {
671 | return _this.autoScroll.scroll();
672 | });
673 | } else if (_this.autoScroll.down) {
674 | $('html, body').dequeue().animate({
675 | scrollTop: $(window).scrollTop() + 50
676 | }, 100, 'easeOutQuad');
677 | return delay(100, function() {
678 | return _this.autoScroll.scroll();
679 | });
680 | } else {
681 | return _this.autoScroll.scrolling = false;
682 | }
683 | };
684 | })(this),
685 | start: (function(_this) {
686 | return function(e) {
687 | if (e.clientY < 50) {
688 | _this.autoScroll.up = true;
689 | _this.autoScroll.down = false;
690 | if (!_this.autoScroll.scrolling) {
691 | return _this.autoScroll.scroll();
692 | }
693 | } else if (e.clientY > $(window).innerHeight() - 50) {
694 | _this.autoScroll.up = false;
695 | _this.autoScroll.down = true;
696 | if (!_this.autoScroll.scrolling) {
697 | return _this.autoScroll.scroll();
698 | }
699 | } else {
700 | _this.autoScroll.up = false;
701 | return _this.autoScroll.down = false;
702 | }
703 | };
704 | })(this),
705 | stop: (function(_this) {
706 | return function() {
707 | _this.autoScroll.up = false;
708 | return _this.autoScroll.down = false;
709 | };
710 | })(this)
711 | };
712 | this.dragMirrorMode = (function(_this) {
713 | return function($element, defer, object) {
714 | var result;
715 | if (defer == null) {
716 | defer = true;
717 | }
718 | result = {
719 | id: _this.getNewId(),
720 | mode: 'mirror',
721 | maternal: $element[0],
722 | element: null,
723 | object: object
724 | };
725 | $element.on('mousedown', function(e) {
726 | var $clone;
727 | e.preventDefault();
728 | $clone = $element.clone();
729 | result.element = $clone[0];
730 | $clone.addClass("fb-draggable form-horizontal prepare-dragging");
731 | _this.hooks.move.drag = function(e, defer) {
732 | var droppable, id, _ref, _results;
733 | if ($clone.hasClass('prepare-dragging')) {
734 | $clone.css({
735 | width: $element.width(),
736 | height: $element.height()
737 | });
738 | $clone.removeClass('prepare-dragging');
739 | $clone.addClass('dragging');
740 | if (defer) {
741 | return;
742 | }
743 | }
744 | $clone.offset({
745 | left: e.pageX - $clone.width() / 2,
746 | top: e.pageY - $clone.height() / 2
747 | });
748 | _this.autoScroll.start(e);
749 | _ref = _this.data.droppables;
750 | _results = [];
751 | for (id in _ref) {
752 | droppable = _ref[id];
753 | if (_this.isHover($clone, $(droppable.element))) {
754 | _results.push(droppable.move(e, result));
755 | } else {
756 | _results.push(droppable.out(e, result));
757 | }
758 | }
759 | return _results;
760 | };
761 | _this.hooks.up.drag = function(e) {
762 | var droppable, id, isHover, _ref;
763 | _ref = _this.data.droppables;
764 | for (id in _ref) {
765 | droppable = _ref[id];
766 | isHover = _this.isHover($clone, $(droppable.element));
767 | droppable.up(e, isHover, result);
768 | }
769 | delete _this.hooks.move.drag;
770 | delete _this.hooks.up.drag;
771 | result.element = null;
772 | $clone.remove();
773 | return _this.autoScroll.stop();
774 | };
775 | $('body').append($clone);
776 | if (!defer) {
777 | return _this.hooks.move.drag(e, defer);
778 | }
779 | });
780 | return result;
781 | };
782 | })(this);
783 | this.dragDragMode = (function(_this) {
784 | return function($element, defer, object) {
785 | var result;
786 | if (defer == null) {
787 | defer = true;
788 | }
789 | result = {
790 | id: _this.getNewId(),
791 | mode: 'drag',
792 | maternal: null,
793 | element: $element[0],
794 | object: object
795 | };
796 | $element.addClass('fb-draggable');
797 | $element.on('mousedown', function(e) {
798 | e.preventDefault();
799 | if ($element.hasClass('dragging')) {
800 | return;
801 | }
802 | $element.addClass('prepare-dragging');
803 | _this.hooks.move.drag = function(e, defer) {
804 | var droppable, id, _ref;
805 | if ($element.hasClass('prepare-dragging')) {
806 | $element.css({
807 | width: $element.width(),
808 | height: $element.height()
809 | });
810 | $element.removeClass('prepare-dragging');
811 | $element.addClass('dragging');
812 | if (defer) {
813 | return;
814 | }
815 | }
816 | $element.offset({
817 | left: e.pageX - $element.width() / 2,
818 | top: e.pageY - $element.height() / 2
819 | });
820 | _this.autoScroll.start(e);
821 | _ref = _this.data.droppables;
822 | for (id in _ref) {
823 | droppable = _ref[id];
824 | if (_this.isHover($element, $(droppable.element))) {
825 | droppable.move(e, result);
826 | } else {
827 | droppable.out(e, result);
828 | }
829 | }
830 | };
831 | _this.hooks.up.drag = function(e) {
832 | var droppable, id, isHover, _ref;
833 | _ref = _this.data.droppables;
834 | for (id in _ref) {
835 | droppable = _ref[id];
836 | isHover = _this.isHover($element, $(droppable.element));
837 | droppable.up(e, isHover, result);
838 | }
839 | delete _this.hooks.move.drag;
840 | delete _this.hooks.up.drag;
841 | $element.css({
842 | width: '',
843 | height: '',
844 | left: '',
845 | top: ''
846 | });
847 | $element.removeClass('dragging defer-dragging');
848 | return _this.autoScroll.stop();
849 | };
850 | if (!defer) {
851 | return _this.hooks.move.drag(e, defer);
852 | }
853 | });
854 | return result;
855 | };
856 | })(this);
857 | this.dropMode = (function(_this) {
858 | return function($element, options) {
859 | var result;
860 | result = {
861 | id: _this.getNewId(),
862 | element: $element[0],
863 | move: function(e, draggable) {
864 | return $rootScope.$apply(function() {
865 | return typeof options.move === "function" ? options.move(e, draggable) : void 0;
866 | });
867 | },
868 | up: function(e, isHover, draggable) {
869 | return $rootScope.$apply(function() {
870 | return typeof options.up === "function" ? options.up(e, isHover, draggable) : void 0;
871 | });
872 | },
873 | out: function(e, draggable) {
874 | return $rootScope.$apply(function() {
875 | return typeof options.out === "function" ? options.out(e, draggable) : void 0;
876 | });
877 | }
878 | };
879 | return result;
880 | };
881 | })(this);
882 | this.draggable = (function(_this) {
883 | return function($element, options) {
884 | var draggable, element, result, _i, _j, _len, _len1;
885 | if (options == null) {
886 | options = {};
887 | }
888 |
889 | /*
890 | Make the element could be drag.
891 | @param element: The jQuery element.
892 | @param options: Options
893 | mode: 'drag' [default], 'mirror'
894 | defer: yes/no. defer dragging
895 | object: custom information
896 | */
897 | result = [];
898 | if (options.mode === 'mirror') {
899 | for (_i = 0, _len = $element.length; _i < _len; _i++) {
900 | element = $element[_i];
901 | draggable = _this.dragMirrorMode($(element), options.defer, options.object);
902 | result.push(draggable.id);
903 | _this.data.draggables[draggable.id] = draggable;
904 | }
905 | } else {
906 | for (_j = 0, _len1 = $element.length; _j < _len1; _j++) {
907 | element = $element[_j];
908 | draggable = _this.dragDragMode($(element), options.defer, options.object);
909 | result.push(draggable.id);
910 | _this.data.draggables[draggable.id] = draggable;
911 | }
912 | }
913 | return result;
914 | };
915 | })(this);
916 | this.droppable = (function(_this) {
917 | return function($element, options) {
918 | var droppable, element, result, _i, _len;
919 | if (options == null) {
920 | options = {};
921 | }
922 |
923 | /*
924 | Make the element coulde be drop.
925 | @param $element: The jQuery element.
926 | @param options: The droppable options.
927 | move: The custom mouse move callback. (e, draggable)->
928 | up: The custom mouse up callback. (e, isHover, draggable)->
929 | out: The custom mouse out callback. (e, draggable)->
930 | */
931 | result = [];
932 | for (_i = 0, _len = $element.length; _i < _len; _i++) {
933 | element = $element[_i];
934 | droppable = _this.dropMode($(element), options);
935 | result.push(droppable);
936 | _this.data.droppables[droppable.id] = droppable;
937 | }
938 | return result;
939 | };
940 | })(this);
941 | this.get = function($injector) {
942 | this.setupEasing();
943 | this.setupProviders($injector);
944 | return {
945 | isMouseMoved: this.isMouseMoved,
946 | data: this.data,
947 | draggable: this.draggable,
948 | droppable: this.droppable
949 | };
950 | };
951 | this.get.$inject = ['$injector'];
952 | this.$get = this.get;
953 | });
954 |
955 | }).call(this);
956 |
957 | (function() {
958 | angular.module('builder', ['builder.directive']);
959 |
960 | }).call(this);
961 |
962 |
963 | /*
964 | component:
965 | It is like a class.
966 | The base components are textInput, textArea, select, check, radio.
967 | User can custom the form with components.
968 | formObject:
969 | It is like an object (an instance of the component).
970 | User can custom the label, description, required and validation of the input.
971 | form:
972 | This is for end-user. There are form groups int the form.
973 | They can input the value to the form.
974 | */
975 |
976 | (function() {
977 | var __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
978 |
979 | angular.module('builder.provider', []).provider('$builder', function() {
980 | var $http, $injector, $templateCache;
981 | $injector = null;
982 | $http = null;
983 | $templateCache = null;
984 | this.config = {
985 | popoverPlacement: 'right'
986 | };
987 | this.components = {};
988 | this.groups = [];
989 | this.broadcastChannel = {
990 | updateInput: '$updateInput'
991 | };
992 | this.forms = {
993 | "default": []
994 | };
995 | this.convertComponent = function(name, component) {
996 | var result, _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7, _ref8, _ref9;
997 | result = {
998 | name: name,
999 | group: (_ref = component.group) != null ? _ref : 'Default',
1000 | label: (_ref1 = component.label) != null ? _ref1 : '',
1001 | description: (_ref2 = component.description) != null ? _ref2 : '',
1002 | placeholder: (_ref3 = component.placeholder) != null ? _ref3 : '',
1003 | editable: (_ref4 = component.editable) != null ? _ref4 : true,
1004 | required: (_ref5 = component.required) != null ? _ref5 : false,
1005 | validation: (_ref6 = component.validation) != null ? _ref6 : '/.*/',
1006 | validationOptions: (_ref7 = component.validationOptions) != null ? _ref7 : [],
1007 | options: (_ref8 = component.options) != null ? _ref8 : [],
1008 | arrayToText: (_ref9 = component.arrayToText) != null ? _ref9 : false,
1009 | template: component.template,
1010 | templateUrl: component.templateUrl,
1011 | popoverTemplate: component.popoverTemplate,
1012 | popoverTemplateUrl: component.popoverTemplateUrl
1013 | };
1014 | if (!result.template && !result.templateUrl) {
1015 | console.error("The template is empty.");
1016 | }
1017 | if (!result.popoverTemplate && !result.popoverTemplateUrl) {
1018 | console.error("The popoverTemplate is empty.");
1019 | }
1020 | return result;
1021 | };
1022 | this.convertFormObject = function(name, formObject) {
1023 | var component, result, _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7;
1024 | if (formObject == null) {
1025 | formObject = {};
1026 | }
1027 | component = this.components[formObject.component];
1028 | if (component == null) {
1029 | throw "The component " + formObject.component + " was not registered.";
1030 | }
1031 | result = {
1032 | id: formObject.id,
1033 | component: formObject.component,
1034 | editable: (_ref = formObject.editable) != null ? _ref : component.editable,
1035 | index: (_ref1 = formObject.index) != null ? _ref1 : 0,
1036 | label: (_ref2 = formObject.label) != null ? _ref2 : component.label,
1037 | description: (_ref3 = formObject.description) != null ? _ref3 : component.description,
1038 | placeholder: (_ref4 = formObject.placeholder) != null ? _ref4 : component.placeholder,
1039 | options: (_ref5 = formObject.options) != null ? _ref5 : component.options,
1040 | required: (_ref6 = formObject.required) != null ? _ref6 : component.required,
1041 | validation: (_ref7 = formObject.validation) != null ? _ref7 : component.validation
1042 | };
1043 | return result;
1044 | };
1045 | this.reindexFormObject = (function(_this) {
1046 | return function(name) {
1047 | var formObjects, index, _i, _ref;
1048 | formObjects = _this.forms[name];
1049 | for (index = _i = 0, _ref = formObjects.length; _i < _ref; index = _i += 1) {
1050 | formObjects[index].index = index;
1051 | }
1052 | };
1053 | })(this);
1054 | this.setupProviders = (function(_this) {
1055 | return function(injector) {
1056 | $injector = injector;
1057 | $http = $injector.get('$http');
1058 | return $templateCache = $injector.get('$templateCache');
1059 | };
1060 | })(this);
1061 | this.loadTemplate = function(component) {
1062 |
1063 | /*
1064 | Load template for components.
1065 | @param component: {object} The component of $builder.
1066 | */
1067 | if (component.template == null) {
1068 | $http.get(component.templateUrl, {
1069 | cache: $templateCache
1070 | }).success(function(template) {
1071 | return component.template = template;
1072 | });
1073 | }
1074 | if (component.popoverTemplate == null) {
1075 | return $http.get(component.popoverTemplateUrl, {
1076 | cache: $templateCache
1077 | }).success(function(template) {
1078 | return component.popoverTemplate = template;
1079 | });
1080 | }
1081 | };
1082 | this.registerComponent = (function(_this) {
1083 | return function(name, component) {
1084 | var newComponent, _ref;
1085 | if (component == null) {
1086 | component = {};
1087 | }
1088 |
1089 | /*
1090 | Register the component for form-builder.
1091 | @param name: The component name.
1092 | @param component: The component object.
1093 | group: {string} The component group.
1094 | label: {string} The label of the input.
1095 | description: {string} The description of the input.
1096 | placeholder: {string} The placeholder of the input.
1097 | editable: {bool} Is the form object editable?
1098 | required: {bool} Is the form object required?
1099 | validation: {string} angular-validator. "/regex/" or "[rule1, rule2]". (default is RegExp(.*))
1100 | validationOptions: {array} [{rule: angular-validator, label: 'option label'}] the options for the validation. (default is [])
1101 | options: {array} The input options.
1102 | arrayToText: {bool} checkbox could use this to convert input (default is no)
1103 | template: {string} html template
1104 | templateUrl: {string} The url of the template.
1105 | popoverTemplate: {string} html template
1106 | popoverTemplateUrl: {string} The url of the popover template.
1107 | */
1108 | if (_this.components[name] == null) {
1109 | newComponent = _this.convertComponent(name, component);
1110 | _this.components[name] = newComponent;
1111 | if ($injector != null) {
1112 | _this.loadTemplate(newComponent);
1113 | }
1114 | if (_ref = newComponent.group, __indexOf.call(_this.groups, _ref) < 0) {
1115 | _this.groups.push(newComponent.group);
1116 | }
1117 | } else {
1118 | console.error("The component " + name + " was registered.");
1119 | }
1120 | };
1121 | })(this);
1122 | this.addFormObject = (function(_this) {
1123 | return function(name, formObject) {
1124 | var _base;
1125 | if (formObject == null) {
1126 | formObject = {};
1127 | }
1128 |
1129 | /*
1130 | Insert the form object into the form at last.
1131 | */
1132 | if ((_base = _this.forms)[name] == null) {
1133 | _base[name] = [];
1134 | }
1135 | return _this.insertFormObject(name, _this.forms[name].length, formObject);
1136 | };
1137 | })(this);
1138 | this.insertFormObject = (function(_this) {
1139 | return function(name, index, formObject) {
1140 | var _base;
1141 | if (formObject == null) {
1142 | formObject = {};
1143 | }
1144 |
1145 | /*
1146 | Insert the form object into the form at {index}.
1147 | @param name: The form name.
1148 | @param index: The form object index.
1149 | @param form: The form object.
1150 | id: The form object id.
1151 | component: {string} The component name
1152 | editable: {bool} Is the form object editable? (default is yes)
1153 | label: {string} The form object label.
1154 | description: {string} The form object description.
1155 | placeholder: {string} The form object placeholder.
1156 | options: {array} The form object options.
1157 | required: {bool} Is the form object required? (default is no)
1158 | validation: {string} angular-validator. "/regex/" or "[rule1, rule2]".
1159 | [index]: {int} The form object index. It will be updated by $builder.
1160 | @return: The form object.
1161 | */
1162 | if ((_base = _this.forms)[name] == null) {
1163 | _base[name] = [];
1164 | }
1165 | if (index > _this.forms[name].length) {
1166 | index = _this.forms[name].length;
1167 | } else if (index < 0) {
1168 | index = 0;
1169 | }
1170 | _this.forms[name].splice(index, 0, _this.convertFormObject(name, formObject));
1171 | _this.reindexFormObject(name);
1172 | return _this.forms[name][index];
1173 | };
1174 | })(this);
1175 | this.removeFormObject = (function(_this) {
1176 | return function(name, index) {
1177 |
1178 | /*
1179 | Remove the form object by the index.
1180 | @param name: The form name.
1181 | @param index: The form object index.
1182 | */
1183 | var formObjects;
1184 | formObjects = _this.forms[name];
1185 | formObjects.splice(index, 1);
1186 | return _this.reindexFormObject(name);
1187 | };
1188 | })(this);
1189 | this.updateFormObjectIndex = (function(_this) {
1190 | return function(name, oldIndex, newIndex) {
1191 |
1192 | /*
1193 | Update the index of the form object.
1194 | @param name: The form name.
1195 | @param oldIndex: The old index.
1196 | @param newIndex: The new index.
1197 | */
1198 | var formObject, formObjects;
1199 | if (oldIndex === newIndex) {
1200 | return;
1201 | }
1202 | formObjects = _this.forms[name];
1203 | formObject = formObjects.splice(oldIndex, 1)[0];
1204 | formObjects.splice(newIndex, 0, formObject);
1205 | return _this.reindexFormObject(name);
1206 | };
1207 | })(this);
1208 | this.$get = [
1209 | '$injector', (function(_this) {
1210 | return function($injector) {
1211 | var component, name, _ref;
1212 | _this.setupProviders($injector);
1213 | _ref = _this.components;
1214 | for (name in _ref) {
1215 | component = _ref[name];
1216 | _this.loadTemplate(component);
1217 | }
1218 | return {
1219 | config: _this.config,
1220 | components: _this.components,
1221 | groups: _this.groups,
1222 | forms: _this.forms,
1223 | broadcastChannel: _this.broadcastChannel,
1224 | registerComponent: _this.registerComponent,
1225 | addFormObject: _this.addFormObject,
1226 | insertFormObject: _this.insertFormObject,
1227 | removeFormObject: _this.removeFormObject,
1228 | updateFormObjectIndex: _this.updateFormObjectIndex
1229 | };
1230 | };
1231 | })(this)
1232 | ];
1233 | });
1234 |
1235 | }).call(this);
1236 |
--------------------------------------------------------------------------------
/dist/angular-form-builder.min.js:
--------------------------------------------------------------------------------
1 | (function(){var a;a=function(a,b){var c,d;for(c in a)d=a[c],"$$hashKey"!==c&&(b[c]=d)},angular.module("builder.controller",["builder.provider"]).controller("fbFormObjectEditableController",["$scope","$injector",function(b,c){var d;return d=c.get("$builder"),b.setupScope=function(c){var e;return a(c,b),b.optionsText=c.options.join("\n"),b.$watch("[label, description, placeholder, required, options, validation]",function(){return c.label=b.label,c.description=b.description,c.placeholder=b.placeholder,c.required=b.required,c.options=b.options,c.validation=b.validation},!0),b.$watch("optionsText",function(a){var c;return b.options=function(){var b,d,e,f;for(e=a.split("\n"),f=[],b=0,d=e.length;b0&&f.push(c);return f}(),b.inputText=b.options[0]}),e=d.components[c.component],b.validationOptions=e.validationOptions},b.data={model:null,backup:function(){return this.model={label:b.label,description:b.description,placeholder:b.placeholder,required:b.required,optionsText:b.optionsText,validation:b.validation}},rollback:function(){if(this.model)return b.label=this.model.label,b.description=this.model.description,b.placeholder=this.model.placeholder,b.required=this.model.required,b.optionsText=this.model.optionsText,b.validation=this.model.validation}}}]).controller("fbComponentsController",["$scope","$injector",function(a,b){var c;return c=b.get("$builder"),a.selectGroup=function(b,d){var e,f,g,h;null!=b&&b.preventDefault(),a.activeGroup=d,a.components=[],g=c.components,h=[];for(f in g)e=g[f],e.group===d&&h.push(a.components.push(e));return h},a.groups=c.groups,a.activeGroup=a.groups[0],a.allComponents=c.components,a.$watch("allComponents",function(){return a.selectGroup(null,a.activeGroup)})}]).controller("fbComponentController",["$scope",function(b){return b.copyObjectToScope=function(c){return a(c,b)}}]).controller("fbFormController",["$scope","$injector",function(a,b){var c,d;return c=b.get("$builder"),d=b.get("$timeout"),null==a.input&&(a.input=[]),a.$watch("form",function(){return a.input.length>a.form.length&&a.input.splice(a.form.length),d(function(){return a.$broadcast(c.broadcastChannel.updateInput)})},!0)}]).controller("fbFormObjectController",["$scope","$injector",function(b,c){return c.get("$builder"),b.copyObjectToScope=function(c){return a(c,b)},b.updateInput=function(a){var c;return c={id:b.formObject.id,label:b.formObject.label,value:null!=a?a:""},b.$parent.input.splice(b.$index,1,c)}}])}).call(this),function(){angular.module("builder.directive",["builder.provider","builder.controller","builder.drag","validator"]).directive("fbBuilder",["$injector",function(a){var b,c;return b=a.get("$builder"),c=a.get("$drag"),{restrict:"A",scope:{fbBuilder:"="},template:"",link:function(a,d,e){var f,g,h;return a.formName=e.fbBuilder,null==(g=b.forms)[h=a.formName]&&(g[h]=[]),a.formObjects=b.forms[a.formName],f=!0,$(d).addClass("fb-builder"),c.droppable($(d),{move:function(a){var b,c,e,g,h,i,j,k,l,m,n;if(f&&($("div.fb-form-object-editable").popover("hide"),f=!1),e=$(d).find(".fb-form-object-editable:not(.empty,.dragging)"),0===e.length)return void(0===$(d).find(".fb-form-object-editable.empty").length&&$(d).find(">div:first").append($("")));for(j=[],j.push(-1e3),h=k=0,m=e.length;kj[h-1]&&a.pageY<=j[h]){$(d).find(".empty").remove(),b=$(""),h-10?(b=$(f).offset().top+$(f).height()/2,c=b-a.height()/2,a.css({position:"absolute",top:c}),a.show(),setTimeout(function(){return a.addClass("in"),$(f).triggerHandler("shown.bs.popover")},0),!1):void 0)}),$(f).on("shown.bs.popover",function(){$(".popover ."+g.id+" input:first").select(),a.$apply(function(){return a.popover.shown()})}),$(f).on("hide.bs.popover",function(){var b;return b=$("form."+g.id).closest(".popover"),g.isClickedSave||(a.$$phase||a.$root.$$phase?a.popover.cancel():a.$apply(function(){return a.popover.cancel()})),b.removeClass("in"),setTimeout(function(){return b.hide()},300),!1})}}}]).directive("fbComponents",function(){return{restrict:"A",template:'\n',controller:"fbComponentsController"}}).directive("fbComponent",["$injector",function(a){var b,c;return a.get("$builder"),c=a.get("$drag"),b=a.get("$compile"),{restrict:"A",scope:{component:"=fbComponent"},controller:"fbComponentController",link:function(a,d){return a.copyObjectToScope(a.component),c.draggable($(d),{mode:"mirror",defer:!1,object:{componentName:a.component.name}}),a.$watch("component.template",function(c){var e;if(c)return e=b(c)(a),$(d).html(e)})}}}]).directive("fbForm",["$injector",function(a){return{restrict:"A",require:"ngModel",scope:{formName:"@fbForm",input:"=ngModel",default:"=fbDefault"},template:'',controller:"fbFormController",link:function(b,c,d){var e,f,g;return e=a.get("$builder"),null==(f=e.forms)[g=b.formName]&&(f[g]=[]),b.form=e.forms[b.formName]}}}]).directive("fbFormObject",["$injector",function(a){var b,c,d;return b=a.get("$builder"),c=a.get("$compile"),d=a.get("$parse"),{restrict:"A",controller:"fbFormObjectController",link:function(a,e,f){return a.formObject=d(f.fbFormObject)(a),a.$component=b.components[a.formObject.component],a.$on(b.broadcastChannel.updateInput,function(){return a.updateInput(a.inputText)}),a.$component.arrayToText&&(a.inputArray=[],a.$watch("inputArray",function(b,c){var d,e,f;if(b!==c){d=[];for(e in a.inputArray)a.inputArray[e]&&d.push(null!=(f=a.options[e])?f:a.inputArray[e]);return a.inputText=d.join(", ")}},!0)),a.$watch("inputText",function(){return a.updateInput(a.inputText)}),a.$watch(f.fbFormObject,function(){return a.copyObjectToScope(a.formObject)},!0),a.$watch("$component.template",function(b){var d,f,g;if(b)return f=$(b),d=f.find("[ng-model='inputText']"),d.attr({validator:"{{validation}}"}),g=c(f)(a),$(e).html(g)}),!a.$component.arrayToText&&a.formObject.options.length>0&&(a.inputText=a.formObject.options[0]),a.$watch("default['"+a.formObject.id+"']",function(b){if(b)return a.$component.arrayToText?a.inputArray=b:a.inputText=b})}}}])}.call(this),function(){angular.module("builder.drag",[]).provider("$drag",function(){var a,b,c;a=null,b=null,this.data={draggables:{},droppables:{}},this.mouseMoved=!1,this.isMouseMoved=function(a){return function(){return a.mouseMoved}}(this),this.hooks={down:{},move:{},up:{}},this.eventMouseMove=function(){},this.eventMouseUp=function(){},$(function(a){return function(){return $(document).on("mousedown",function(b){var c,d;a.mouseMoved=!1,d=a.hooks.down;for(c in d)(0,d[c])(b)}),$(document).on("mousemove",function(b){var c,d;a.mouseMoved=!0,d=a.hooks.move;for(c in d)(0,d[c])(b)}),$(document).on("mouseup",function(b){var c,d;d=a.hooks.up;for(c in d)(0,d[c])(b)})}}(this)),this.currentId=0,this.getNewId=function(a){return function(){return""+a.currentId++}}(this),this.setupEasing=function(){return jQuery.extend(jQuery.easing,{easeOutQuad:function(a,b,c,d,e){return-d*(b/=e)*(b-2)+c}})},this.setupProviders=function(c){return a=c,b=a.get("$rootScope")},this.isHover=function(a){return function(a,b){var c,d,e,f,g;return d=a.offset(),e=b.offset(),f={width:a.width(),height:a.height()},g={width:b.width(),height:b.height()},c={x:!1,y:!1},c.x=d.left>e.left&&d.lefte.left&&d.left+f.widthe.top&&d.tope.top&&d.top+f.height$(window).innerHeight()-50))return a.autoScroll.up=!1,a.autoScroll.down=!1;if(a.autoScroll.up=!1,a.autoScroll.down=!0,!a.autoScroll.scrolling)return a.autoScroll.scroll()}}}(this),stop:function(a){return function(){return a.autoScroll.up=!1,a.autoScroll.down=!1}}(this)},this.dragMirrorMode=function(a){return function(b,c,d){var e;return null==c&&(c=!0),e={id:a.getNewId(),mode:"mirror",maternal:b[0],element:null,object:d},b.on("mousedown",function(d){var f;if(d.preventDefault(),f=b.clone(),e.element=f[0],f.addClass("fb-draggable form-horizontal prepare-dragging"),a.hooks.move.drag=function(c,d){var g,h,i,j;if(!f.hasClass("prepare-dragging")||(f.css({width:b.width(),height:b.height()}),f.removeClass("prepare-dragging"),f.addClass("dragging"),!d)){f.offset({left:c.pageX-f.width()/2,top:c.pageY-f.height()/2}),a.autoScroll.start(c),i=a.data.droppables,j=[];for(h in i)g=i[h],a.isHover(f,$(g.element))?j.push(g.move(c,e)):j.push(g.out(c,e));return j}},a.hooks.up.drag=function(b){var c,d,g,h;h=a.data.droppables;for(d in h)c=h[d],g=a.isHover(f,$(c.element)),c.up(b,g,e);return delete a.hooks.move.drag,delete a.hooks.up.drag,e.element=null,f.remove(),a.autoScroll.stop()},$("body").append(f),!c)return a.hooks.move.drag(d,c)}),e}}(this),this.dragDragMode=function(a){return function(b,c,d){var e;return null==c&&(c=!0),e={id:a.getNewId(),mode:"drag",maternal:null,element:b[0],object:d},b.addClass("fb-draggable"),b.on("mousedown",function(d){if(d.preventDefault(),!b.hasClass("dragging"))return b.addClass("prepare-dragging"),a.hooks.move.drag=function(c,d){var f,g,h;if(!b.hasClass("prepare-dragging")||(b.css({width:b.width(),height:b.height()}),b.removeClass("prepare-dragging"),b.addClass("dragging"),!d)){b.offset({left:c.pageX-b.width()/2,top:c.pageY-b.height()/2}),a.autoScroll.start(c),h=a.data.droppables;for(g in h)f=h[g],a.isHover(b,$(f.element))?f.move(c,e):f.out(c,e)}},a.hooks.up.drag=function(c){var d,f,g,h;h=a.data.droppables;for(f in h)d=h[f],g=a.isHover(b,$(d.element)),d.up(c,g,e);return delete a.hooks.move.drag,delete a.hooks.up.drag,b.css({width:"",height:"",left:"",top:""}),b.removeClass("dragging defer-dragging"),a.autoScroll.stop()},c?void 0:a.hooks.move.drag(d,c)}),e}}(this),this.dropMode=function(a){return function(c,d){return{id:a.getNewId(),element:c[0],move:function(a,c){return b.$apply(function(){return"function"==typeof d.move?d.move(a,c):void 0})},up:function(a,c,e){return b.$apply(function(){return"function"==typeof d.up?d.up(a,c,e):void 0})},out:function(a,c){return b.$apply(function(){return"function"==typeof d.out?d.out(a,c):void 0})}}}}(this),this.draggable=function(a){return function(b,c){var d,e,f,g,h,i,j;if(null==c&&(c={}),f=[],"mirror"===c.mode)for(g=0,i=b.length;ga.forms[b].length?c=a.forms[b].length:c<0&&(c=0),a.forms[b].splice(c,0,a.convertFormObject(b,d)),a.reindexFormObject(b),a.forms[b][c]}}(this),this.removeFormObject=function(a){return function(b,c){var d;return d=a.forms[b],d.splice(c,1),a.reindexFormObject(b)}}(this),this.updateFormObjectIndex=function(a){return function(b,c,d){var e,f;if(c!==d)return f=a.forms[b],e=f.splice(c,1)[0],f.splice(d,0,e),a.reindexFormObject(b)}}(this),this.$get=["$injector",function(a){return function(b){var c,d,e;a.setupProviders(b),e=a.components;for(d in e)c=e[d],a.loadTemplate(c);return{config:a.config,components:a.components,groups:a.groups,forms:a.forms,broadcastChannel:a.broadcastChannel,registerComponent:a.registerComponent,addFormObject:a.addFormObject,insertFormObject:a.insertFormObject,removeFormObject:a.removeFormObject,updateFormObjectIndex:a.updateFormObjectIndex}}}(this)]})}.call(this);
--------------------------------------------------------------------------------
/example/demo.coffee:
--------------------------------------------------------------------------------
1 | angular.module 'app', ['builder', 'builder.components', 'validator.rules']
2 |
3 | .run ['$builder', ($builder) ->
4 | $builder.registerComponent 'sampleInput',
5 | group: 'from html'
6 | label: 'Sample'
7 | description: 'From html template'
8 | placeholder: 'placeholder'
9 | required: no
10 | validationOptions: [
11 | {label: 'none', rule: '/.*/'}
12 | {label: 'number', rule: '[number]'}
13 | {label: 'email', rule: '[email]'}
14 | {label: 'url', rule: '[url]'}
15 | ]
16 | templateUrl: 'example/template.html'
17 | popoverTemplateUrl: 'example/popoverTemplate.html'
18 |
19 | # ----------------------------------------
20 | # two text input
21 | # ----------------------------------------
22 | $builder.registerComponent 'name',
23 | group: 'Default'
24 | label: 'Name'
25 | required: no
26 | arrayToText: yes
27 | template:
28 | """
29 |
47 | """
48 | popoverTemplate:
49 | """
50 |
69 | """
70 | ]
71 |
72 |
73 | .controller 'DemoController', ['$scope', '$builder', '$validator', ($scope, $builder, $validator) ->
74 | # ----------------------------------------
75 | # builder
76 | # ----------------------------------------
77 | textbox = $builder.addFormObject 'default',
78 | id: 'textbox'
79 | component: 'textInput'
80 | label: 'Name'
81 | description: 'Your name'
82 | placeholder: 'Your name'
83 | required: yes
84 | editable: no
85 | checkbox = $builder.addFormObject 'default',
86 | id: 'checkbox'
87 | component: 'checkbox'
88 | label: 'Pets'
89 | description: 'Do you have any pets?'
90 | options: ['Dog', 'Cat']
91 | $builder.addFormObject 'default',
92 | component: 'sampleInput'
93 | # formObjects
94 | $scope.form = $builder.forms['default']
95 |
96 | # ----------------------------------------
97 | # form
98 | # ----------------------------------------
99 | # user input value
100 | $scope.input = []
101 | $scope.defaultValue = {}
102 | # formObjectId: default value
103 | $scope.defaultValue[textbox.id] = 'default value'
104 | $scope.defaultValue[checkbox.id] = [yes, yes]
105 |
106 | $scope.submit = ->
107 | $validator.validate $scope, 'default'
108 | .success -> console.log 'success'
109 | .error -> console.log 'error'
110 | ]
--------------------------------------------------------------------------------
/example/demo.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | angular.module('app', ['builder', 'builder.components', 'validator.rules']).run([
3 | '$builder', function($builder) {
4 | $builder.registerComponent('sampleInput', {
5 | group: 'from html',
6 | label: 'Sample',
7 | description: 'From html template',
8 | placeholder: 'placeholder',
9 | required: false,
10 | validationOptions: [
11 | {
12 | label: 'none',
13 | rule: '/.*/'
14 | }, {
15 | label: 'number',
16 | rule: '[number]'
17 | }, {
18 | label: 'email',
19 | rule: '[email]'
20 | }, {
21 | label: 'url',
22 | rule: '[url]'
23 | }
24 | ],
25 | templateUrl: 'example/template.html',
26 | popoverTemplateUrl: 'example/popoverTemplate.html'
27 | });
28 | return $builder.registerComponent('name', {
29 | group: 'Default',
30 | label: 'Name',
31 | required: false,
32 | arrayToText: true,
33 | template: "",
34 | popoverTemplate: ""
35 | });
36 | }
37 | ]).controller('DemoController', [
38 | '$scope', '$builder', '$validator', function($scope, $builder, $validator) {
39 | var checkbox, textbox;
40 | textbox = $builder.addFormObject('default', {
41 | id: 'textbox',
42 | component: 'textInput',
43 | label: 'Name',
44 | description: 'Your name',
45 | placeholder: 'Your name',
46 | required: true,
47 | editable: false
48 | });
49 | checkbox = $builder.addFormObject('default', {
50 | id: 'checkbox',
51 | component: 'checkbox',
52 | label: 'Pets',
53 | description: 'Do you have any pets?',
54 | options: ['Dog', 'Cat']
55 | });
56 | $builder.addFormObject('default', {
57 | component: 'sampleInput'
58 | });
59 | $scope.form = $builder.forms['default'];
60 | $scope.input = [];
61 | $scope.defaultValue = {};
62 | $scope.defaultValue[textbox.id] = 'default value';
63 | $scope.defaultValue[checkbox.id] = [true, true];
64 | return $scope.submit = function() {
65 | return $validator.validate($scope, 'default').success(function() {
66 | return console.log('success');
67 | }).error(function() {
68 | return console.log('error');
69 | });
70 | };
71 | }
72 | ]);
73 |
74 | }).call(this);
75 |
--------------------------------------------------------------------------------
/example/popoverTemplate.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/site.css:
--------------------------------------------------------------------------------
1 | body{padding:10px}.footer{height:50px}
2 |
--------------------------------------------------------------------------------
/example/site.scss:
--------------------------------------------------------------------------------
1 | body {
2 | padding: 10px;
3 | }
4 | .footer {
5 | height: 50px;
6 | }
--------------------------------------------------------------------------------
/example/template.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | angular-form-builder
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
angular-form-builder
22 |
23 |
24 |
25 |
26 |
27 |
Builder
28 |
29 |
30 |
38 |
39 |
40 |
41 |
44 |
45 |
46 |
47 |
Form
48 |
49 |
57 |
58 |
61 |
62 |
{{input}}
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-form-builder",
3 | "version": "0.1.0",
4 | "description": "AngularJS form builder.",
5 | "scripts": {
6 | "test": "grunt karma"
7 | },
8 | "homepage": "https://github.com/kelp404/angular-form-builder",
9 | "repository": {
10 | "type": "git",
11 | "url": "git://github.com/kelp404/angular-form-builder.git"
12 | },
13 | "keywords": [
14 | "AngularJS",
15 | "CoffeeScript"
16 | ],
17 | "author": {
18 | "name": "Kelp",
19 | "url": "https://github.com/kelp404"
20 | },
21 | "contributors": [
22 | {
23 | "name": "others",
24 | "url": "https://github.com/kelp404/angular-form-builder/graphs/contributors"
25 | }
26 | ],
27 | "license": "MIT",
28 | "bugs": {
29 | "url": "https://github.com/kelp404/angular-form-builder/issues"
30 | },
31 | "devDependencies": {
32 | "angular": "1.2.32",
33 | "angular-mocks": "1.2.32",
34 | "angular-validator": "git+https://git@github.com/kelp404/angular-validator.git#0.2.8",
35 | "grunt": "0.4.2",
36 | "grunt-contrib-coffee": "0.12.0",
37 | "grunt-contrib-compass": "1.0.1",
38 | "grunt-contrib-connect": "0.8.0",
39 | "grunt-contrib-uglify": "0.6.0",
40 | "grunt-contrib-watch": "0.6.1",
41 | "grunt-karma": "0.9.0",
42 | "jquery": "3.3.1",
43 | "jasmine-core": "2.9.1",
44 | "karma": "2.0.0",
45 | "karma-coffee-preprocessor": "1.0.1",
46 | "karma-jasmine": "1.1.1",
47 | "karma-phantomjs-launcher": "1.0.4",
48 | "phantomjs-prebuilt": "2.1.16"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/angular-form-builder.scss:
--------------------------------------------------------------------------------
1 |
2 | @import "compass/css3";
3 |
4 |
5 | .fb-component {
6 | padding: 10px;
7 | cursor: move;
8 | input { cursor: move; }
9 | label { cursor: move; }
10 | select { cursor: move; }
11 | textarea { cursor: move; }
12 | }
13 | .fb-form-object-editable {
14 | padding: 10px;
15 | }
16 | .fb-form-object-editable.fb-draggable {
17 | cursor: move;
18 | input { cursor: move; }
19 | label { cursor: pointer; }
20 | select { cursor: move; }
21 | textarea { cursor: move; }
22 | }
23 | .fb-form-object-editable.empty {
24 | cursor: default;
25 | margin: 6px;
26 | height: 80px;
27 | border:dashed 1px #aaa;
28 | background-color: #eee;
29 | }
30 |
31 | .fb-draggable.dragging {
32 | background-color: #ffffff;
33 | position: absolute;
34 | z-index: 800;
35 | @include box-shadow(#666 0 0 20px);
36 | }
37 |
38 | .fb-required:after {
39 | color: #b94a48;
40 | content: ' *';
41 | }
42 |
43 | .fb-builder {
44 | min-height: 250px;
45 | }
46 |
47 |
48 | .popover {
49 | .control-label {
50 | text-align: left;
51 | }
52 | form {
53 | width: 240px;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/controller.coffee:
--------------------------------------------------------------------------------
1 | # ----------------------------------------
2 | # Shared functions
3 | # ----------------------------------------
4 | copyObjectToScope = (object, scope) ->
5 | ###
6 | Copy object (ng-repeat="object in objects") to scope without `hashKey`.
7 | ###
8 | for key, value of object when key isnt '$$hashKey'
9 | # copy object.{} to scope.{}
10 | scope[key] = value
11 | return
12 |
13 |
14 | # ----------------------------------------
15 | # builder.controller
16 | # ----------------------------------------
17 | angular.module 'builder.controller', ['builder.provider']
18 |
19 | # ----------------------------------------
20 | # fbFormObjectEditableController
21 | # ----------------------------------------
22 | .controller 'fbFormObjectEditableController', ['$scope', '$injector', ($scope, $injector) ->
23 | $builder = $injector.get '$builder'
24 |
25 | $scope.setupScope = (formObject) ->
26 | ###
27 | 1. Copy origin formObject (ng-repeat="object in formObjects") to scope.
28 | 2. Setup optionsText with formObject.options.
29 | 3. Watch scope.label, .description, .placeholder, .required, .options then copy to origin formObject.
30 | 4. Watch scope.optionsText then convert to scope.options.
31 | 5. setup validationOptions
32 | ###
33 | copyObjectToScope formObject, $scope
34 |
35 | $scope.optionsText = formObject.options.join '\n'
36 |
37 | $scope.$watch '[label, description, placeholder, required, options, validation]', ->
38 | formObject.label = $scope.label
39 | formObject.description = $scope.description
40 | formObject.placeholder = $scope.placeholder
41 | formObject.required = $scope.required
42 | formObject.options = $scope.options
43 | formObject.validation = $scope.validation
44 | , yes
45 |
46 | $scope.$watch 'optionsText', (text) ->
47 | $scope.options = (x for x in text.split('\n') when x.length > 0)
48 | $scope.inputText = $scope.options[0]
49 |
50 | component = $builder.components[formObject.component]
51 | $scope.validationOptions = component.validationOptions
52 |
53 | $scope.data =
54 | model: null
55 | backup: ->
56 | ###
57 | Backup input value.
58 | ###
59 | @model =
60 | label: $scope.label
61 | description: $scope.description
62 | placeholder: $scope.placeholder
63 | required: $scope.required
64 | optionsText: $scope.optionsText
65 | validation: $scope.validation
66 | rollback: ->
67 | ###
68 | Rollback input value.
69 | ###
70 | return if not @model
71 | $scope.label = @model.label
72 | $scope.description = @model.description
73 | $scope.placeholder = @model.placeholder
74 | $scope.required = @model.required
75 | $scope.optionsText = @model.optionsText
76 | $scope.validation = @model.validation
77 | ]
78 |
79 |
80 | # ----------------------------------------
81 | # fbComponentsController
82 | # ----------------------------------------
83 | .controller 'fbComponentsController', ['$scope', '$injector', ($scope, $injector) ->
84 | # providers
85 | $builder = $injector.get '$builder'
86 |
87 | # action
88 | $scope.selectGroup = ($event, group) ->
89 | $event?.preventDefault()
90 | $scope.activeGroup = group
91 | $scope.components = []
92 | for name, component of $builder.components when component.group is group
93 | $scope.components.push component
94 |
95 | $scope.groups = $builder.groups
96 | $scope.activeGroup = $scope.groups[0]
97 | $scope.allComponents = $builder.components
98 | $scope.$watch 'allComponents', -> $scope.selectGroup null, $scope.activeGroup
99 | ]
100 |
101 |
102 | # ----------------------------------------
103 | # fbComponentController
104 | # ----------------------------------------
105 | .controller 'fbComponentController', ['$scope', ($scope) ->
106 | $scope.copyObjectToScope = (object) -> copyObjectToScope object, $scope
107 | ]
108 |
109 |
110 | # ----------------------------------------
111 | # fbFormController
112 | # ----------------------------------------
113 | .controller 'fbFormController', ['$scope', '$injector', ($scope, $injector) ->
114 | # providers
115 | $builder = $injector.get '$builder'
116 | $timeout = $injector.get '$timeout'
117 |
118 | # set default for input
119 | $scope.input ?= []
120 | $scope.$watch 'form', ->
121 | # remove superfluous input
122 | if $scope.input.length > $scope.form.length
123 | $scope.input.splice $scope.form.length
124 | # tell children to update input value.
125 | # ! use $timeout for waiting $scope updated.
126 | $timeout ->
127 | $scope.$broadcast $builder.broadcastChannel.updateInput
128 | , yes
129 | ]
130 |
131 |
132 | # ----------------------------------------
133 | # fbFormObjectController
134 | # ----------------------------------------
135 | .controller 'fbFormObjectController', ['$scope', '$injector', ($scope, $injector) ->
136 | # providers
137 | $builder = $injector.get '$builder'
138 |
139 | $scope.copyObjectToScope = (object) -> copyObjectToScope object, $scope
140 |
141 | $scope.updateInput = (value) ->
142 | ###
143 | Copy current scope.input[X] to $parent.input.
144 | @param value: The input value.
145 | ###
146 | input =
147 | id: $scope.formObject.id
148 | label: $scope.formObject.label
149 | value: value ? ''
150 | $scope.$parent.input.splice $scope.$index, 1, input
151 | ]
152 |
--------------------------------------------------------------------------------
/src/directive.coffee:
--------------------------------------------------------------------------------
1 |
2 | # ----------------------------------------
3 | # builder.directive
4 | # ----------------------------------------
5 | angular.module 'builder.directive', [
6 | 'builder.provider'
7 | 'builder.controller'
8 | 'builder.drag'
9 | 'validator'
10 | ]
11 |
12 |
13 | # ----------------------------------------
14 | # fb-builder
15 | # ----------------------------------------
16 | .directive 'fbBuilder', ['$injector', ($injector) ->
17 | # providers
18 | $builder = $injector.get '$builder'
19 | $drag = $injector.get '$drag'
20 |
21 | restrict: 'A'
22 | scope:
23 | fbBuilder: '='
24 | template:
25 | """
26 |
30 | """
31 | link: (scope, element, attrs) ->
32 | # ----------------------------------------
33 | # valuables
34 | # ----------------------------------------
35 | scope.formName = attrs.fbBuilder
36 | $builder.forms[scope.formName] ?= []
37 | scope.formObjects = $builder.forms[scope.formName]
38 | beginMove = yes
39 |
40 | $(element).addClass 'fb-builder'
41 | $drag.droppable $(element),
42 | move: (e) ->
43 | if beginMove
44 | # hide all popovers
45 | $("div.fb-form-object-editable").popover 'hide'
46 | beginMove = no
47 |
48 | $formObjects = $(element).find '.fb-form-object-editable:not(.empty,.dragging)'
49 | if $formObjects.length is 0
50 | # there are no components in the builder.
51 | if $(element).find('.fb-form-object-editable.empty').length is 0
52 | $(element).find('>div:first').append $("")
53 | return
54 |
55 | # the positions could added .empty div.
56 | positions = []
57 | # first
58 | positions.push -1000
59 | for index in [0...$formObjects.length] by 1
60 | $formObject = $($formObjects[index])
61 | offset = $formObject.offset()
62 | height = $formObject.height()
63 | positions.push offset.top + height / 2
64 | positions.push positions[positions.length - 1] + 1000 # last
65 |
66 | # search where should I insert the .empty
67 | for index in [1...positions.length] by 1
68 | if e.pageY > positions[index - 1] and e.pageY <= positions[index]
69 | # you known, this one
70 | $(element).find('.empty').remove()
71 | $empty = $ ""
72 | if index - 1 < $formObjects.length
73 | $empty.insertBefore $($formObjects[index - 1])
74 | else
75 | $empty.insertAfter $($formObjects[index - 2])
76 | break
77 | return
78 | out: ->
79 | if beginMove
80 | # hide all popovers
81 | $("div.fb-form-object-editable").popover 'hide'
82 | beginMove = no
83 |
84 | $(element).find('.empty').remove()
85 | up: (e, isHover, draggable) ->
86 | beginMove = yes
87 | if not $drag.isMouseMoved()
88 | # click event
89 | $(element).find('.empty').remove()
90 | return
91 |
92 | if not isHover and draggable.mode is 'drag'
93 | # remove the form object by draggin out
94 | formObject = draggable.object.formObject
95 | if formObject.editable
96 | $builder.removeFormObject attrs.fbBuilder, formObject.index
97 | else if isHover
98 | if draggable.mode is 'mirror'
99 | # insert a form object
100 | $builder.insertFormObject scope.formName, $(element).find('.empty').index('.fb-form-object-editable'),
101 | component: draggable.object.componentName
102 | if draggable.mode is 'drag'
103 | # update the index of form objects
104 | oldIndex = draggable.object.formObject.index
105 | newIndex = $(element).find('.empty').index('.fb-form-object-editable')
106 | newIndex-- if oldIndex < newIndex
107 | $builder.updateFormObjectIndex scope.formName, oldIndex, newIndex
108 | $(element).find('.empty').remove()
109 | ]
110 |
111 | # ----------------------------------------
112 | # fb-form-object-editable
113 | # ----------------------------------------
114 | .directive 'fbFormObjectEditable', ['$injector', ($injector) ->
115 | # providers
116 | $builder = $injector.get '$builder'
117 | $drag = $injector.get '$drag'
118 | $compile = $injector.get '$compile'
119 | $validator = $injector.get '$validator'
120 |
121 | restrict: 'A'
122 | controller: 'fbFormObjectEditableController'
123 | scope:
124 | formObject: '=fbFormObjectEditable'
125 | link: (scope, element) ->
126 | scope.inputArray = [] # just for fix warning
127 | # get component
128 | scope.$component = $builder.components[scope.formObject.component]
129 | # setup scope
130 | scope.setupScope scope.formObject
131 |
132 | # compile formObject
133 | scope.$watch '$component.template', (template) ->
134 | return if not template
135 | view = $compile(template) scope
136 | $(element).html view
137 |
138 | # disable click event
139 | $(element).on 'click', -> no
140 |
141 | # draggable
142 | $drag.draggable $(element),
143 | object:
144 | formObject: scope.formObject
145 |
146 | # do not setup bootstrap popover
147 | return if not scope.formObject.editable
148 |
149 | # ----------------------------------------
150 | # bootstrap popover
151 | # ----------------------------------------
152 | popover = {}
153 | scope.$watch '$component.popoverTemplate', (template) ->
154 | return if not template
155 | $(element).removeClass popover.id
156 | popover =
157 | id: "fb-#{Math.random().toString().substr(2)}"
158 | isClickedSave: no # If didn't click save then rollback
159 | view: null
160 | html: template
161 | popover.html = $(popover.html).addClass popover.id
162 | # compile popover
163 | popover.view = $compile(popover.html) scope
164 | $(element).addClass popover.id
165 | $(element).popover
166 | html: yes
167 | title: scope.$component.label
168 | content: popover.view
169 | container: 'body'
170 | placement: $builder.config.popoverPlacement
171 | scope.popover =
172 | save: ($event) ->
173 | ###
174 | The save event of the popover.
175 | ###
176 | $event.preventDefault()
177 | $validator.validate(scope).success ->
178 | popover.isClickedSave = yes
179 | $(element).popover 'hide'
180 | return
181 | remove: ($event) ->
182 | ###
183 | The delete event of the popover.
184 | ###
185 | $event.preventDefault()
186 |
187 | $builder.removeFormObject scope.$parent.formName, scope.$parent.$index
188 | $(element).popover 'hide'
189 | return
190 | shown: ->
191 | ###
192 | The shown event of the popover.
193 | ###
194 | scope.data.backup()
195 | popover.isClickedSave = no
196 | cancel: ($event) ->
197 | ###
198 | The cancel event of the popover.
199 | ###
200 | scope.data.rollback()
201 | if $event
202 | # clicked cancel by user
203 | $event.preventDefault()
204 | $(element).popover 'hide'
205 | return
206 | # ----------------------------------------
207 | # popover.show
208 | # ----------------------------------------
209 | $(element).on 'show.bs.popover', ->
210 | return no if $drag.isMouseMoved()
211 | # hide other popovers
212 | $("div.fb-form-object-editable:not(.#{popover.id})").popover 'hide'
213 |
214 | $popover = $("form.#{popover.id}").closest '.popover'
215 | if $popover.length > 0
216 | # fixed offset
217 | elementOrigin = $(element).offset().top + $(element).height() / 2
218 | popoverTop = elementOrigin - $popover.height() / 2
219 | $popover.css
220 | position: 'absolute'
221 | top: popoverTop
222 |
223 | $popover.show()
224 | setTimeout ->
225 | $popover.addClass 'in'
226 | $(element).triggerHandler 'shown.bs.popover'
227 | , 0
228 | no
229 | # ----------------------------------------
230 | # popover.shown
231 | # ----------------------------------------
232 | $(element).on 'shown.bs.popover', ->
233 | # select the first input
234 | $(".popover .#{popover.id} input:first").select()
235 | scope.$apply -> scope.popover.shown()
236 | return
237 | # ----------------------------------------
238 | # popover.hide
239 | # ----------------------------------------
240 | $(element).on 'hide.bs.popover', ->
241 | # do not remove the DOM
242 | $popover = $("form.#{popover.id}").closest '.popover'
243 | if not popover.isClickedSave
244 | # eval the cancel event
245 | if scope.$$phase or scope.$root.$$phase
246 | scope.popover.cancel()
247 | else
248 | scope.$apply -> scope.popover.cancel()
249 | $popover.removeClass 'in'
250 | setTimeout ->
251 | $popover.hide()
252 | , 300
253 | no
254 | ]
255 |
256 |
257 | # ----------------------------------------
258 | # fb-components
259 | # ----------------------------------------
260 | .directive 'fbComponents', ->
261 | restrict: 'A'
262 | template:
263 | """
264 |
269 |
273 | """
274 | controller: 'fbComponentsController'
275 |
276 | # ----------------------------------------
277 | # fb-component
278 | # ----------------------------------------
279 | .directive 'fbComponent', ['$injector', ($injector) ->
280 | # providers
281 | $builder = $injector.get '$builder'
282 | $drag = $injector.get '$drag'
283 | $compile = $injector.get '$compile'
284 |
285 | restrict: 'A'
286 | scope:
287 | component: '=fbComponent'
288 | controller: 'fbComponentController'
289 | link: (scope, element) ->
290 | scope.copyObjectToScope scope.component
291 |
292 | $drag.draggable $(element),
293 | mode: 'mirror'
294 | defer: no
295 | object:
296 | componentName: scope.component.name
297 |
298 | scope.$watch 'component.template', (template) ->
299 | return if not template
300 | view = $compile(template) scope
301 | $(element).html view
302 | ]
303 |
304 |
305 | # ----------------------------------------
306 | # fb-form
307 | # ----------------------------------------
308 | .directive 'fbForm', ['$injector', ($injector) ->
309 | restrict: 'A'
310 | require: 'ngModel' # form data (end-user input value)
311 | scope:
312 | # input model for scops in ng-repeat
313 | formName: '@fbForm'
314 | input: '=ngModel'
315 | default: '=fbDefault'
316 | template:
317 | """
318 |
319 | """
320 | controller: 'fbFormController'
321 | link: (scope, element, attrs) ->
322 | # providers
323 | $builder = $injector.get '$builder'
324 |
325 | # get the form for controller
326 | $builder.forms[scope.formName] ?= []
327 | scope.form = $builder.forms[scope.formName]
328 | ]
329 |
330 | # ----------------------------------------
331 | # fb-form-object
332 | # ----------------------------------------
333 | .directive 'fbFormObject', ['$injector', ($injector) ->
334 | # providers
335 | $builder = $injector.get '$builder'
336 | $compile = $injector.get '$compile'
337 | $parse = $injector.get '$parse'
338 |
339 | restrict: 'A'
340 | controller: 'fbFormObjectController'
341 | link: (scope, element, attrs) ->
342 | # ----------------------------------------
343 | # variables
344 | # ----------------------------------------
345 | scope.formObject = $parse(attrs.fbFormObject) scope
346 | scope.$component = $builder.components[scope.formObject.component]
347 |
348 | # ----------------------------------------
349 | # scope
350 | # ----------------------------------------
351 | # listen (formObject updated
352 | scope.$on $builder.broadcastChannel.updateInput, -> scope.updateInput scope.inputText
353 | if scope.$component.arrayToText
354 | scope.inputArray = []
355 | # watch (end-user updated input of the form
356 | scope.$watch 'inputArray', (newValue, oldValue) ->
357 | # array input, like checkbox
358 | return if newValue is oldValue
359 | checked = []
360 | for index of scope.inputArray when scope.inputArray[index]
361 | checked.push scope.options[index] ? scope.inputArray[index]
362 | scope.inputText = checked.join ', '
363 | , yes
364 | scope.$watch 'inputText', -> scope.updateInput scope.inputText
365 | # watch (management updated form objects
366 | scope.$watch attrs.fbFormObject, ->
367 | scope.copyObjectToScope scope.formObject
368 | , yes
369 |
370 | scope.$watch '$component.template', (template) ->
371 | return if not template
372 | $template = $(template)
373 | # add validator
374 | $input = $template.find "[ng-model='inputText']"
375 | $input.attr
376 | validator: '{{validation}}'
377 | # compile
378 | view = $compile($template) scope
379 | $(element).html view
380 |
381 | # select the first option
382 | if not scope.$component.arrayToText and scope.formObject.options.length > 0
383 | scope.inputText = scope.formObject.options[0]
384 |
385 | # set default value
386 | scope.$watch "default['#{scope.formObject.id}']", (value) ->
387 | return if not value
388 | if scope.$component.arrayToText
389 | scope.inputArray = value
390 | else
391 | scope.inputText = value
392 | ]
393 |
--------------------------------------------------------------------------------
/src/drag.coffee:
--------------------------------------------------------------------------------
1 | angular.module 'builder.drag', []
2 |
3 | .provider '$drag', ->
4 | # ----------------------------------------
5 | # provider
6 | # ----------------------------------------
7 | $injector = null
8 | $rootScope = null
9 |
10 |
11 | # ----------------------------------------
12 | # properties
13 | # ----------------------------------------
14 | @data =
15 | # all draggable objects
16 | draggables: {}
17 | # all droppable objects
18 | droppables: {}
19 |
20 |
21 | # ----------------------------------------
22 | # event hooks
23 | # ----------------------------------------
24 | @mouseMoved = no
25 | @isMouseMoved = => @mouseMoved
26 | @hooks =
27 | down: {}
28 | move: {}
29 | up: {}
30 | @eventMouseMove = ->
31 | @eventMouseUp = ->
32 | $ =>
33 | $(document).on 'mousedown', (e) =>
34 | @mouseMoved = no
35 | func(e) for key, func of @hooks.down
36 | return
37 | $(document).on 'mousemove', (e) =>
38 | @mouseMoved = yes
39 | func(e) for key, func of @hooks.move
40 | return
41 | $(document).on 'mouseup', (e) =>
42 | func(e) for key, func of @hooks.up
43 | return
44 |
45 |
46 | # ----------------------------------------
47 | # private methods
48 | # ----------------------------------------
49 | @currentId = 0
50 | @getNewId = => "#{@currentId++}"
51 |
52 |
53 | @setupEasing = ->
54 | jQuery.extend jQuery.easing,
55 | easeOutQuad: (x, t, b, c, d) -> -c * (t /= d) * (t - 2) + b
56 |
57 |
58 | @setupProviders = (injector) ->
59 | ###
60 | Setup providers.
61 | ###
62 | $injector = injector
63 | $rootScope = $injector.get '$rootScope'
64 |
65 |
66 | @isHover = ($elementA, $elementB) =>
67 | ###
68 | Is element A hover on element B?
69 | @param $elementA: jQuery object
70 | @param $elementB: jQuery object
71 | ###
72 | offsetA = $elementA.offset()
73 | offsetB = $elementB.offset()
74 | sizeA =
75 | width: $elementA.width()
76 | height: $elementA.height()
77 | sizeB =
78 | width: $elementB.width()
79 | height: $elementB.height()
80 | isHover =
81 | x: no
82 | y: no
83 | # x
84 | isHover.x = offsetA.left > offsetB.left and offsetA.left < offsetB.left + sizeB.width
85 | isHover.x = isHover.x or offsetA.left + sizeA.width > offsetB.left and offsetA.left + sizeA.width < offsetB.left + sizeB.width
86 | return no if not isHover
87 | # y
88 | isHover.y = offsetA.top > offsetB.top and offsetA.top < offsetB.top + sizeB.height
89 | isHover.y = isHover.y or offsetA.top + sizeA.height > offsetB.top and offsetA.top + sizeA.height < offsetB.top + sizeB.height
90 | isHover.x and isHover.y
91 |
92 |
93 | delay = (ms, func) ->
94 | setTimeout ->
95 | func()
96 | , ms
97 | @autoScroll =
98 | up: no
99 | down: no
100 | scrolling: no
101 | scroll: =>
102 | @autoScroll.scrolling = yes
103 | if @autoScroll.up
104 | $('html, body').dequeue().animate
105 | scrollTop: $(window).scrollTop() - 50
106 | , 100, 'easeOutQuad'
107 | delay 100, => @autoScroll.scroll()
108 | else if @autoScroll.down
109 | $('html, body').dequeue().animate
110 | scrollTop: $(window).scrollTop() + 50
111 | , 100, 'easeOutQuad'
112 | delay 100, => @autoScroll.scroll()
113 | else
114 | @autoScroll.scrolling = no
115 | start: (e) =>
116 | if e.clientY < 50
117 | # up
118 | @autoScroll.up = yes
119 | @autoScroll.down = no
120 | @autoScroll.scroll() if not @autoScroll.scrolling
121 | else if e.clientY > $(window).innerHeight() - 50
122 | # down
123 | @autoScroll.up = no
124 | @autoScroll.down = yes
125 | @autoScroll.scroll() if not @autoScroll.scrolling
126 | else
127 | @autoScroll.up = no
128 | @autoScroll.down = no
129 | stop: =>
130 | @autoScroll.up = no
131 | @autoScroll.down = no
132 |
133 |
134 | @dragMirrorMode = ($element, defer=yes, object) =>
135 | result =
136 | id: @getNewId()
137 | mode: 'mirror'
138 | maternal: $element[0]
139 | element: null
140 | object: object
141 |
142 | $element.on 'mousedown', (e) =>
143 | e.preventDefault()
144 |
145 | $clone = $element.clone()
146 | result.element = $clone[0]
147 | $clone.addClass "fb-draggable form-horizontal prepare-dragging"
148 | @hooks.move.drag = (e, defer) =>
149 | if $clone.hasClass 'prepare-dragging'
150 | $clone.css
151 | width: $element.width()
152 | height: $element.height()
153 | $clone.removeClass 'prepare-dragging'
154 | $clone.addClass 'dragging'
155 | return if defer
156 |
157 | $clone.offset
158 | left: e.pageX - $clone.width() / 2
159 | top: e.pageY - $clone.height() / 2
160 |
161 | @autoScroll.start e
162 |
163 | # execute callback for droppables
164 | for id, droppable of @data.droppables
165 | if @isHover $clone, $(droppable.element)
166 | droppable.move e, result
167 | else
168 | droppable.out e, result
169 | @hooks.up.drag = (e) =>
170 | # execute callback for droppables
171 | for id, droppable of @data.droppables
172 | isHover = @isHover $clone, $(droppable.element)
173 | droppable.up e, isHover, result
174 | delete @hooks.move.drag
175 | delete @hooks.up.drag
176 | result.element = null
177 | $clone.remove()
178 | @autoScroll.stop()
179 | $('body').append $clone
180 | # setup left & top of the element
181 | @hooks.move.drag(e, defer) if not defer
182 | result
183 |
184 |
185 | @dragDragMode = ($element, defer=yes, object) =>
186 | result =
187 | id: @getNewId()
188 | mode: 'drag'
189 | maternal: null
190 | element: $element[0]
191 | object: object
192 |
193 | $element.addClass 'fb-draggable'
194 | $element.on 'mousedown', (e) =>
195 | e.preventDefault()
196 | return if $element.hasClass 'dragging'
197 |
198 | $element.addClass 'prepare-dragging'
199 | @hooks.move.drag = (e, defer) =>
200 | if $element.hasClass 'prepare-dragging'
201 | $element.css
202 | width: $element.width()
203 | height: $element.height()
204 | $element.removeClass 'prepare-dragging'
205 | $element.addClass 'dragging'
206 | return if defer
207 |
208 | $element.offset
209 | left: e.pageX - $element.width() / 2
210 | top: e.pageY - $element.height() / 2
211 |
212 | @autoScroll.start e
213 |
214 | # execute callback for droppables
215 | for id, droppable of @data.droppables
216 | if @isHover $element, $(droppable.element)
217 | droppable.move e, result
218 | else
219 | droppable.out e, result
220 | return
221 | @hooks.up.drag = (e) =>
222 | # execute callback for droppables
223 | for id, droppable of @data.droppables
224 | isHover = @isHover $element, $(droppable.element)
225 | droppable.up e, isHover, result
226 |
227 | delete @hooks.move.drag
228 | delete @hooks.up.drag
229 | $element.css
230 | width: '', height: ''
231 | left: '', top: ''
232 | $element.removeClass 'dragging defer-dragging'
233 | @autoScroll.stop()
234 | # setup left & top of the element
235 | @hooks.move.drag(e, defer) if not defer
236 | result
237 |
238 |
239 | @dropMode = ($element, options) =>
240 | result =
241 | id: @getNewId()
242 | element: $element[0]
243 | move: (e, draggable) ->
244 | $rootScope.$apply -> options.move?(e, draggable)
245 | up: (e, isHover, draggable) ->
246 | $rootScope.$apply -> options.up?(e, isHover, draggable)
247 | out: (e, draggable) ->
248 | $rootScope.$apply -> options.out?(e, draggable)
249 | result
250 | # ----------------------------------------
251 | # public methods
252 | # ----------------------------------------
253 | @draggable = ($element, options={}) =>
254 | ###
255 | Make the element could be drag.
256 | @param element: The jQuery element.
257 | @param options: Options
258 | mode: 'drag' [default], 'mirror'
259 | defer: yes/no. defer dragging
260 | object: custom information
261 | ###
262 | result = []
263 | if options.mode is 'mirror'
264 | for element in $element
265 | draggable = @dragMirrorMode $(element), options.defer, options.object
266 | result.push draggable.id
267 | @data.draggables[draggable.id] = draggable
268 | else
269 | for element in $element
270 | draggable = @dragDragMode $(element), options.defer, options.object
271 | result.push draggable.id
272 | @data.draggables[draggable.id] = draggable
273 | result
274 |
275 |
276 | @droppable = ($element, options={}) =>
277 | ###
278 | Make the element coulde be drop.
279 | @param $element: The jQuery element.
280 | @param options: The droppable options.
281 | move: The custom mouse move callback. (e, draggable)->
282 | up: The custom mouse up callback. (e, isHover, draggable)->
283 | out: The custom mouse out callback. (e, draggable)->
284 | ###
285 | result = []
286 | for element in $element
287 | droppable = @dropMode $(element), options
288 | result.push droppable
289 | @data.droppables[droppable.id] = droppable
290 | result
291 |
292 |
293 | # ----------------------------------------
294 | # $get
295 | # ----------------------------------------
296 | @get = ($injector) ->
297 | @setupEasing()
298 | @setupProviders $injector
299 |
300 | isMouseMoved: @isMouseMoved
301 | data: @data
302 | draggable: @draggable
303 | droppable: @droppable
304 | @get.$inject = ['$injector']
305 | @$get = @get
306 | return
307 |
--------------------------------------------------------------------------------
/src/module.coffee:
--------------------------------------------------------------------------------
1 | angular.module 'builder', ['builder.directive']
--------------------------------------------------------------------------------
/src/provider.coffee:
--------------------------------------------------------------------------------
1 | ###
2 | component:
3 | It is like a class.
4 | The base components are textInput, textArea, select, check, radio.
5 | User can custom the form with components.
6 | formObject:
7 | It is like an object (an instance of the component).
8 | User can custom the label, description, required and validation of the input.
9 | form:
10 | This is for end-user. There are form groups int the form.
11 | They can input the value to the form.
12 | ###
13 |
14 | angular.module 'builder.provider', []
15 |
16 | .provider '$builder', ->
17 | $injector = null
18 | $http = null
19 | $templateCache = null
20 |
21 | @config =
22 | popoverPlacement: 'right'
23 | # all components
24 | @components = {}
25 | # all groups of components
26 | @groups = []
27 | @broadcastChannel =
28 | updateInput: '$updateInput'
29 |
30 | # forms
31 | # builder mode: `fb-builder` you could drag and drop to build the form.
32 | # form mode: `fb-form` this is the form for end-user to input value.
33 | @forms =
34 | default: []
35 |
36 |
37 | # ----------------------------------------
38 | # private functions
39 | # ----------------------------------------
40 | @convertComponent = (name, component) ->
41 | result =
42 | name: name
43 | group: component.group ? 'Default'
44 | label: component.label ? ''
45 | description: component.description ? ''
46 | placeholder: component.placeholder ? ''
47 | editable: component.editable ? yes
48 | required: component.required ? no
49 | validation: component.validation ? '/.*/'
50 | validationOptions: component.validationOptions ? []
51 | options: component.options ? []
52 | arrayToText: component.arrayToText ? no
53 | template: component.template
54 | templateUrl: component.templateUrl
55 | popoverTemplate: component.popoverTemplate
56 | popoverTemplateUrl: component.popoverTemplateUrl
57 | if not result.template and not result.templateUrl
58 | console.error "The template is empty."
59 | if not result.popoverTemplate and not result.popoverTemplateUrl
60 | console.error "The popoverTemplate is empty."
61 | result
62 |
63 | @convertFormObject = (name, formObject={}) ->
64 | component = @components[formObject.component]
65 | throw "The component #{formObject.component} was not registered." if not component?
66 | result =
67 | id: formObject.id
68 | component: formObject.component
69 | editable: formObject.editable ? component.editable
70 | index: formObject.index ? 0
71 | label: formObject.label ? component.label
72 | description: formObject.description ? component.description
73 | placeholder: formObject.placeholder ? component.placeholder
74 | options: formObject.options ? component.options
75 | required: formObject.required ? component.required
76 | validation: formObject.validation ? component.validation
77 | result
78 |
79 | @reindexFormObject = (name) =>
80 | formObjects = @forms[name]
81 | for index in [0...formObjects.length] by 1
82 | formObjects[index].index = index
83 | return
84 |
85 | @setupProviders = (injector) =>
86 | $injector = injector
87 | $http = $injector.get '$http'
88 | $templateCache = $injector.get '$templateCache'
89 |
90 | @loadTemplate = (component) ->
91 | ###
92 | Load template for components.
93 | @param component: {object} The component of $builder.
94 | ###
95 | if not component.template?
96 | $http.get component.templateUrl,
97 | cache: $templateCache
98 | .success (template) ->
99 | component.template = template
100 | if not component.popoverTemplate?
101 | $http.get component.popoverTemplateUrl,
102 | cache: $templateCache
103 | .success (template) ->
104 | component.popoverTemplate = template
105 |
106 | # ----------------------------------------
107 | # public functions
108 | # ----------------------------------------
109 | @registerComponent = (name, component={}) =>
110 | ###
111 | Register the component for form-builder.
112 | @param name: The component name.
113 | @param component: The component object.
114 | group: {string} The component group.
115 | label: {string} The label of the input.
116 | description: {string} The description of the input.
117 | placeholder: {string} The placeholder of the input.
118 | editable: {bool} Is the form object editable?
119 | required: {bool} Is the form object required?
120 | validation: {string} angular-validator. "/regex/" or "[rule1, rule2]". (default is RegExp(.*))
121 | validationOptions: {array} [{rule: angular-validator, label: 'option label'}] the options for the validation. (default is [])
122 | options: {array} The input options.
123 | arrayToText: {bool} checkbox could use this to convert input (default is no)
124 | template: {string} html template
125 | templateUrl: {string} The url of the template.
126 | popoverTemplate: {string} html template
127 | popoverTemplateUrl: {string} The url of the popover template.
128 | ###
129 | if not @components[name]?
130 | # regist the new component
131 | newComponent = @convertComponent name, component
132 | @components[name] = newComponent
133 | @loadTemplate(newComponent) if $injector?
134 | if newComponent.group not in @groups
135 | @groups.push newComponent.group
136 | else
137 | console.error "The component #{name} was registered."
138 | return
139 |
140 | @addFormObject = (name, formObject={}) =>
141 | ###
142 | Insert the form object into the form at last.
143 | ###
144 | @forms[name] ?= []
145 | @insertFormObject name, @forms[name].length, formObject
146 |
147 | @insertFormObject = (name, index, formObject={}) =>
148 | ###
149 | Insert the form object into the form at {index}.
150 | @param name: The form name.
151 | @param index: The form object index.
152 | @param form: The form object.
153 | id: The form object id.
154 | component: {string} The component name
155 | editable: {bool} Is the form object editable? (default is yes)
156 | label: {string} The form object label.
157 | description: {string} The form object description.
158 | placeholder: {string} The form object placeholder.
159 | options: {array} The form object options.
160 | required: {bool} Is the form object required? (default is no)
161 | validation: {string} angular-validator. "/regex/" or "[rule1, rule2]".
162 | [index]: {int} The form object index. It will be updated by $builder.
163 | @return: The form object.
164 | ###
165 | @forms[name] ?= []
166 | if index > @forms[name].length then index = @forms[name].length
167 | else if index < 0 then index = 0
168 | @forms[name].splice index, 0, @convertFormObject(name, formObject)
169 | @reindexFormObject name
170 | @forms[name][index]
171 |
172 | @removeFormObject = (name, index) =>
173 | ###
174 | Remove the form object by the index.
175 | @param name: The form name.
176 | @param index: The form object index.
177 | ###
178 | formObjects = @forms[name]
179 | formObjects.splice index, 1
180 | @reindexFormObject name
181 |
182 | @updateFormObjectIndex = (name, oldIndex, newIndex) =>
183 | ###
184 | Update the index of the form object.
185 | @param name: The form name.
186 | @param oldIndex: The old index.
187 | @param newIndex: The new index.
188 | ###
189 | return if oldIndex is newIndex
190 | formObjects = @forms[name]
191 | formObject = formObjects.splice(oldIndex, 1)[0]
192 | formObjects.splice newIndex, 0, formObject
193 | @reindexFormObject name
194 |
195 | # ----------------------------------------
196 | # $get
197 | # ----------------------------------------
198 | @$get = ['$injector', ($injector) =>
199 | @setupProviders($injector)
200 | for name, component of @components
201 | @loadTemplate component
202 |
203 | config: @config
204 | components: @components
205 | groups: @groups
206 | forms: @forms
207 | broadcastChannel: @broadcastChannel
208 | registerComponent: @registerComponent
209 | addFormObject: @addFormObject
210 | insertFormObject: @insertFormObject
211 | removeFormObject: @removeFormObject
212 | updateFormObjectIndex: @updateFormObjectIndex
213 | ]
214 | return
215 |
--------------------------------------------------------------------------------
/test/karma.config.coffee:
--------------------------------------------------------------------------------
1 | module.exports = (config) ->
2 | config.set
3 | # base path, that will be used to resolve files and exclude
4 | basePath: '../'
5 |
6 | frameworks: ['jasmine']
7 |
8 | # list of files / patterns to load in the browser
9 | files: [
10 | 'node_modules/jquery/dist/jquery.min.js'
11 | 'node_modules/angular/angular.min.js'
12 | 'node_modules/angular-mocks/angular-mocks.js'
13 | 'node_modules/angular-validator/dist/angular-validator.js'
14 | 'node_modules/angular-validator/dist/angular-validator-rules.js'
15 | 'dist/angular-form-builder.js'
16 | 'dist/angular-form-builder-components.js'
17 | 'test/specs/*.coffee'
18 | ]
19 |
20 | preprocessors:
21 | '**/*.coffee': 'coffee'
22 |
23 | # list of files to exclude
24 | exclude: []
25 |
26 | # use dots reporter, as travis terminal does not support escaping sequences
27 | # possible values: 'dots', 'progress'
28 | # CLI --reporters progress
29 | reporters: ['progress']
30 |
31 | # web server port
32 | # CLI --port 9876
33 | port: 8081
34 |
35 | # enable / disable colors in the output (reporters and logs)
36 | # CLI --colors --no-colors
37 | colors: yes
38 |
39 | # level of logging
40 | # possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
41 | # CLI --log-level debug
42 | logLevel: config.LOG_DEBUG
43 |
44 | # enable / disable watching file and executing tests whenever any file changes
45 | # CLI --auto-watch --no-auto-watch
46 | autoWatch: no
47 |
48 | # Start these browsers, currently available:
49 | # - Chrome
50 | # - ChromeCanary
51 | # - Firefox
52 | # - Opera
53 | # - Safari (only Mac)
54 | # - PhantomJS
55 | # - IE (only Windows)
56 | # CLI --browsers Chrome,Firefox,Safari
57 | browsers: ['PhantomJS']
58 |
59 | # If browser does not capture in given timeout [ms], kill it
60 | # CLI --capture-timeout 5000
61 | captureTimeout: 20000
62 |
63 | # Auto run tests on start (when browsers are captured) and exit
64 | # CLI --single-run --no-single-run
65 | singleRun: yes
66 |
67 | # report which specs are slower than 500ms
68 | # CLI --report-slower-than 500
69 | reportSlowerThan: 500
70 |
--------------------------------------------------------------------------------
/test/specs/controllerSpec.coffee:
--------------------------------------------------------------------------------
1 | describe 'builder.controller', ->
2 | beforeEach module('builder')
3 |
4 |
5 | describe 'fbFormObjectEditableController', ->
6 | $scope = null
7 | controller = null
8 |
9 | beforeEach inject ($rootScope, $controller, $builder, $injector) ->
10 | $builder.registerComponent 'inputText',
11 | template: ""
12 | popoverTemplate: ""
13 |
14 | $scope = $rootScope.$new()
15 | controller = $controller 'fbFormObjectEditableController',
16 | $scope: $scope
17 | $injector: $injector
18 |
19 | describe '$scope.setupScope()', ->
20 | formObject = null
21 |
22 | beforeEach ->
23 | formObject =
24 | $$hashKey: '007'
25 | component: 'inputText'
26 | label: 'label'
27 | description: 'description'
28 | placeholder: 'placeholder'
29 | required: no
30 | options: ['value one', 'two']
31 | validation: '/.*/'
32 | $scope.setupScope formObject
33 |
34 | it '$scope.setupScope(formObject) copy properties from formObject without `$$hashKey`', ->
35 | expect($scope.$$hashKey).toBeUndefined()
36 | expect(formObject.label).toEqual $scope.label
37 | expect(formObject.description).toEqual $scope.description
38 | expect(formObject.placeholder).toEqual $scope.placeholder
39 | expect(formObject.required).toBe no
40 | expect(formObject.options).toEqual $scope.options
41 | expect(formObject.validation).toEqual $scope.validation
42 |
43 | it '$scope.setupScope(formObject) $scope.optionsText is joined by `\\n` from options', ->
44 | expect($scope.optionsText).toEqual 'value one\ntwo'
45 |
46 | it '$scope.setupScope(formObject) $scope.$watch `[label, description, placeholder, required, options, validation]`', ->
47 | $scope.label = 'new'
48 | $scope.$digest()
49 | expect(formObject.label).toEqual $scope.label
50 |
51 | $scope.description = 'new'
52 | $scope.$digest()
53 | expect(formObject.description).toEqual $scope.description
54 |
55 | $scope.placeholder = 'new'
56 | $scope.$digest()
57 | expect(formObject.placeholder).toEqual $scope.placeholder
58 |
59 | $scope.required = yes
60 | $scope.$digest()
61 | expect(formObject.required).toBe $scope.required
62 |
63 | $scope.options = ['value']
64 | $scope.$digest()
65 | expect(formObject.options).toEqual $scope.options
66 |
67 | $scope.validation = '/regex/'
68 | $scope.$digest()
69 | expect(formObject.validation).toEqual $scope.validation
70 |
71 | it '$scope.setupScope(formObject) $scope.$watch `optionsText`', ->
72 | $scope.optionsText = "one\ntwo"
73 | $scope.$digest()
74 | expect(['one', 'two']).toEqual $scope.options
75 | expect('one').toEqual $scope.inputText
76 |
77 | it '$scope.setupScope(formObject) setup validationOptions', ->
78 | expect([]).toEqual $scope.validationOptions
79 |
80 | describe '$scope.data', ->
81 | formObject = null
82 |
83 | beforeEach ->
84 | formObject =
85 | $$hashKey: '007'
86 | component: 'inputText'
87 | label: 'label'
88 | description: 'description'
89 | placeholder: 'placeholder'
90 | required: no
91 | options: ['value one', 'two']
92 | validation: '/.*/'
93 | $scope.setupScope formObject
94 |
95 | it '$scope.data.model is null', ->
96 | expect($scope.data.model).toBeNull()
97 |
98 | it '$scope.data.model after call $scope.data.backup()', ->
99 | $scope.data.backup()
100 | expect
101 | label: 'label'
102 | description: 'description'
103 | placeholder: 'placeholder'
104 | required: no
105 | optionsText: 'value one\ntwo'
106 | validation: '/.*/'
107 | .toEqual $scope.data.model
108 |
109 | it '$scope after call $scope.data.rollback()', ->
110 | $scope.data.backup()
111 | $scope.label = ''
112 | $scope.description = ''
113 | $scope.placeholder = ''
114 | $scope.required = yes
115 | $scope.optionsText = ''
116 | $scope.data.rollback()
117 | $scope.$digest()
118 | expect(formObject.label).toEqual $scope.label
119 | expect(formObject.description).toEqual $scope.description
120 | expect(formObject.placeholder).toEqual $scope.placeholder
121 | expect(formObject.required).toEqual $scope.required
122 | expect(formObject.options.join('\n')).toEqual $scope.optionsText
123 |
124 |
125 | describe 'fbComponentsController', ->
126 | $scope = null
127 | controller = null
128 |
129 | beforeEach inject ($rootScope, $controller, $builder) ->
130 | $builder.registerComponent 'inputText',
131 | template: ""
132 | popoverTemplate: ""
133 |
134 | $scope = $rootScope.$new()
135 | controller = $controller 'fbComponentsController',
136 | $scope: $scope
137 |
138 | describe '$scope.groups', ->
139 | it '$scope.groups is equal to $builder.groups', inject ($builder) ->
140 | expect($scope.groups).toBe $builder.groups
141 |
142 | describe '$scope.activeGroups', ->
143 | it '$scope.activeGroup is the first on of $scope.groups', inject ->
144 | expect($scope.activeGroup).toBe $scope.groups[0]
145 |
146 | describe '$scope.allComponents', ->
147 | it '$scope.allComponents is equal to $builder.components', inject ($builder) ->
148 | expect($scope.allComponents).toBe $builder.components
149 |
150 | it '$watch $scope.allComponents than call $scope.selectGroup', inject ($builder) ->
151 | spyOn($scope, 'selectGroup').and.callFake ($event, activeGroup) ->
152 | expect($event).toBeNull()
153 | expect($scope.activeGroup).toEqual activeGroup
154 | $builder.registerComponent 'newComponent',
155 | template: ""
156 | popoverTemplate: ""
157 | $scope.$digest()
158 | expect($scope.selectGroup).toHaveBeenCalled()
159 |
160 | describe '$scope.activeGroup()', ->
161 | it '$scope.selectGroup will update activeGroup and components', inject ($builder) ->
162 | $builder.registerComponent 'xComponent',
163 | group: 'X'
164 | template: ""
165 | popoverTemplate: ""
166 | $scope.$digest()
167 |
168 | $event = preventDefault: jasmine.createSpy 'preventDefault'
169 | $scope.selectGroup $event, 'X'
170 | expect($event.preventDefault).toHaveBeenCalled()
171 | expect($scope.activeGroup).toEqual 'X'
172 | expect($scope.components.length).toBe 1
173 | expect($scope.components[0].name).toEqual 'xComponent'
174 |
175 |
176 | describe 'fbComponentController', ->
177 | $scope = null
178 | controller = null
179 |
180 | beforeEach inject ($rootScope, $controller) ->
181 | $scope = $rootScope.$new()
182 | controller = $controller 'fbComponentController',
183 | $scope: $scope
184 |
185 | describe '$scope.copyObjectToScope()', ->
186 | component = null
187 |
188 | beforeEach ->
189 | component =
190 | $$hashKey: '007'
191 | name: 'textInput'
192 | label: 'label'
193 | description: 'description'
194 | placeholder: 'placeholder'
195 | required: no
196 | options: ['value one', 'two']
197 | template: ""
198 | popoverTemplate: ""
199 |
200 | it '$scope.copyObjectToScope(component) copy properties to $scope without `$$hashKey`', ->
201 | $scope.copyObjectToScope component
202 | expect($scope.$$hashKey).toBeUndefined()
203 | expect(component.name).toEqual $scope.name
204 | expect(component.label).toEqual $scope.label
205 | expect(component.description).toEqual $scope.description
206 | expect(component.placeholder).toEqual $scope.placeholder
207 | expect(component.required).toBe no
208 | expect(component.options).toEqual $scope.options
209 | expect(component.template).toEqual $scope.template
210 | expect(component.popoverTemplate).toEqual $scope.popoverTemplate
211 |
212 |
213 | describe 'fbFormController', ->
214 | $timeout = null
215 | $scope = null
216 | controller = null
217 |
218 | beforeEach inject ($rootScope, $controller, $injector) ->
219 | $timeout = $injector.get '$timeout'
220 | $scope = $rootScope.$new()
221 | controller = $controller 'fbFormController',
222 | $scope: $scope
223 | $injector: $injector
224 |
225 | describe "$scope.$watch('form')", ->
226 | it "$scope.$watch('form') remove superfluous input and $broadcast", inject ($builder) ->
227 | spyBroadcast = jasmine.createSpy 'broadcast'
228 | $scope.$on $builder.broadcastChannel.updateInput, ->
229 | spyBroadcast()
230 | $scope.input = [{}, {}]
231 | $scope.form = [
232 | component: 'textbox'
233 | ]
234 | $scope.$digest()
235 | $timeout.flush()
236 | expect($scope.input.length).toBe 1
237 | expect(spyBroadcast).toHaveBeenCalled()
238 |
239 |
240 | describe 'fbFormObjectController', ->
241 | $scope = null
242 | controller = null
243 |
244 | beforeEach inject ($rootScope, $controller, $injector) ->
245 | $scope = $rootScope.$new()
246 | controller = $controller 'fbFormObjectController',
247 | $scope: $scope
248 | $injector: $injector
249 |
250 | describe '$scope.copyObjectToScope()', ->
251 | formObject = null
252 | beforeEach ->
253 | formObject =
254 | $$hashKey: '007'
255 | name: 'textInput'
256 | label: 'label'
257 | description: 'description'
258 | placeholder: 'placeholder'
259 | required: no
260 | options: ['value one', 'two']
261 | it '$scope.copyObjectToScope(formObject) copy properties to $scope without `$$hashKey`', ->
262 | $scope.copyObjectToScope formObject
263 | expect($scope.$$hashKey).toBeUndefined()
264 | expect(formObject.name).toEqual $scope.name
265 | expect(formObject.label).toEqual $scope.label
266 | expect(formObject.description).toEqual $scope.description
267 | expect(formObject.placeholder).toEqual $scope.placeholder
268 | expect(formObject.required).toBe no
269 | expect(formObject.options).toEqual $scope.options
270 |
271 | describe '$scope.updateInput()', ->
272 | it '$scope.updateInput(value) will copy input value to $parent.input', ->
273 | $scope.$parent.input = []
274 | $scope.$index = 0
275 | $scope.formObject =
276 | id: 0
277 | label: 'label'
278 | $scope.updateInput 'value'
279 | expect($scope.$parent.input).toEqual [
280 | id: 0
281 | label: 'label'
282 | value: 'value'
283 | ]
284 |
285 | it '$scope.updateInput(value) will copy input value to $parent.input with default', ->
286 | $scope.$parent.input = []
287 | $scope.$index = 0
288 | $scope.formObject =
289 | id: 0
290 | label: 'label'
291 | $scope.updateInput()
292 | expect($scope.$parent.input).toEqual [
293 | id: 0
294 | label: 'label'
295 | value: ''
296 | ]
297 |
--------------------------------------------------------------------------------
/test/specs/directiveSpec.coffee:
--------------------------------------------------------------------------------
1 | describe 'builder.directive', ->
2 | beforeEach module('builder')
3 |
4 |
5 | describe 'fb-components', ->
6 | $scope = null
7 | $compile = null
8 | $builder = null
9 | template = """"""
10 |
11 | beforeEach inject ($rootScope, $injector) ->
12 | $scope = $rootScope.$new()
13 | $compile = $injector.get '$compile'
14 | $builder = $injector.get '$builder'
15 |
16 | $builder.registerComponent 'textInput',
17 | group: 'Default'
18 | label: 'Text Input'
19 | description: 'description'
20 | placeholder: 'placeholder'
21 | required: no
22 | template:
23 | """
24 |
31 | """
32 | popoverTemplate: """"""
33 |
34 | it 'compile fb-components with a component', ->
35 | view = $compile(template) $scope
36 | $scope.$digest()
37 | expect($(view).find('>.form-horizontal').length).toBe 1
38 | $components = $(view).find '.fb-component'
39 | expect($components.length).toBe 1
40 | expect($components.attr('ng-repeat')).toEqual 'component in components'
41 | expect($components.attr('fb-component')).toEqual 'component'
42 |
43 |
44 | describe 'fb-component', ->
45 | $scope = null
46 | $compile = null
47 | $builder = null
48 | template = """"""
49 |
50 | beforeEach inject ($rootScope, $injector) ->
51 | $scope = $rootScope.$new()
52 | $compile = $injector.get '$compile'
53 | $builder = $injector.get '$builder'
54 |
55 | $builder.registerComponent 'textInput',
56 | group: 'Default'
57 | label: 'Text Input'
58 | description: 'description'
59 | placeholder: 'placeholder'
60 | required: no
61 | template:
62 | """
63 |
70 | """
71 | popoverTemplate: """"""
72 |
73 | it 'compile fb-component and called `scope.copyObjectToScope()`', ->
74 | $compile(template) $scope
75 | $scope.$digest()
76 |
77 | expect($scope.$$childHead).toBe $scope.$$childTail
78 | child = $scope.$$childHead.$$childHead
79 | expect(child.$$hashKey).toBeUndefined()
80 | for key, value of $scope.components[0] when key isnt '$$hashKey'
81 | expect(child[key]).toEqual value
82 |
83 | it 'compile fb-component and called `$drag.draggable()`', inject ($drag) ->
84 | componentName = Object.keys($builder.components)[0]
85 | spyOn($drag, 'draggable').and.callFake ($element, object) ->
86 | expect($element.length).toBe 1
87 | expect($element.hasClass('fb-component')).toBe yes
88 | expect
89 | mode: 'mirror'
90 | defer: no
91 | object:
92 | componentName: componentName
93 | .toEqual object
94 |
95 | $compile(template) $scope
96 | $scope.$digest()
97 | expect($drag.draggable).toHaveBeenCalled()
98 |
99 | it 'compile fb-component, the view is component.template', ->
100 | view = $compile(template) $scope
101 | $scope.$digest()
102 |
103 | $component = $(view).find '.fb-component'
104 | expect($component.length).toBe 1
105 | $formGroup = $component.find '.form-group'
106 | expect($formGroup.length).toBe 1
107 |
108 |
109 | describe 'fb-form', ->
110 | $scope = null
111 | $compile = null
112 | $builder = null
113 | template = """"""
114 |
115 | beforeEach inject ($rootScope, $injector) ->
116 | $scope = $rootScope.$new()
117 | $compile = $injector.get '$compile'
118 | $builder = $injector.get '$builder'
119 |
120 | $builder.registerComponent 'textInput',
121 | group: 'Default'
122 | label: 'Text Input'
123 | description: 'description'
124 | placeholder: 'placeholder'
125 | required: no
126 | template:
127 | """
128 |
135 | """
136 | popoverTemplate: """"""
137 | $builder.addFormObject 'default', component: 'textInput'
138 |
139 | it 'compile fb-form', ->
140 | $scope.input = []
141 | view = $compile(template) $scope
142 | $scope.$digest()
143 | expect($scope.$$childHead).toBe $scope.$$childTail
144 | expect($scope.$$childHead.form).toBe $builder.forms.default
145 | $formObject = $(view).find '.fb-form-object'
146 | expect($formObject.length).toBe 1
147 | expect($formObject.attr('ng-repeat')).toEqual 'object in form'
148 | expect($formObject.attr('fb-form-object')).toEqual 'object'
149 |
--------------------------------------------------------------------------------
/test/specs/providerSpec.coffee:
--------------------------------------------------------------------------------
1 | describe 'builder.provider', ->
2 | fakeModule = null
3 | builderProvider = null
4 |
5 | beforeEach module('builder')
6 | beforeEach ->
7 | fakeModule = angular.module 'fakeModule', ['builder']
8 | fakeModule.config ($builderProvider) ->
9 | builderProvider = $builderProvider
10 | beforeEach module('fakeModule')
11 |
12 |
13 | # ----------------------------------------
14 | # properties
15 | # ----------------------------------------
16 | describe '$builder.components', ->
17 | it '$builder.components is empty', inject ($builder) ->
18 | expect(0).toBe Object.keys($builder.components).length
19 |
20 |
21 | describe '$builder.groups', ->
22 | it '$builder.groups is empty', inject ($builder) ->
23 | expect([]).toEqual $builder.groups
24 |
25 | it '$builder.groups will be updated after registerComponent()', inject ($builder) ->
26 | $builder.registerComponent 'textInput',
27 | group: 'Default'
28 | label: 'Text Input'
29 | description: 'description'
30 | placeholder: 'placeholder'
31 | required: no
32 | template: ""
33 | popoverTemplate: ""
34 | expect(['Default']).toEqual $builder.groups
35 |
36 | $builder.registerComponent 'textArea',
37 | group: 'Default'
38 | label: 'Text Area'
39 | description: 'description'
40 | placeholder: 'placeholder'
41 | required: no
42 | template: ""
43 | popoverTemplate: ""
44 | expect(['Default']).toEqual $builder.groups
45 |
46 | $builder.registerComponent 'textAreaGroupA',
47 | group: 'GroupA'
48 | label: 'Text Area'
49 | description: 'description'
50 | placeholder: 'placeholder'
51 | required: no
52 | template: ""
53 | popoverTemplate: ""
54 | expect(['Default', 'GroupA']).toEqual $builder.groups
55 |
56 |
57 | describe '$builder.broadcastChannel', ->
58 | it '$builder.broadcastChannel', inject ($builder) ->
59 | expect
60 | updateInput: '$updateInput'
61 | .toEqual $builder.broadcastChannel
62 |
63 |
64 | describe '$builder.forms', ->
65 | it '$builder.forms', inject ($builder) ->
66 | expect
67 | default: []
68 | .toEqual $builder.forms
69 |
70 |
71 | # ----------------------------------------
72 | # methods
73 | # ----------------------------------------
74 | describe '$builderProvider.convertComponent()', ->
75 | it '$builderProvider.convertComponent() argument without template', ->
76 | spyOn(console, 'error').and.callFake (msg) ->
77 | expect(msg).toEqual 'The template is empty.'
78 | builderProvider.convertComponent 'inputText',
79 | popoverTemplate: ""
80 | expect(console.error).toHaveBeenCalled()
81 |
82 | it '$builderProvider.convertComponent() argument without popoverTemplate', ->
83 | spyOn(console, 'error').and.callFake (msg) ->
84 | expect(msg).toEqual 'The popoverTemplate is empty.'
85 | builderProvider.convertComponent 'inputText',
86 | template: ""
87 | expect(console.error).toHaveBeenCalled()
88 |
89 | it '$builderProvider.convertComponent() with default', ->
90 | component = builderProvider.convertComponent 'inputText',
91 | template: ""
92 | popoverTemplate: ""
93 | expect
94 | name: 'inputText'
95 | group: 'Default'
96 | label: ''
97 | description: ''
98 | placeholder: ''
99 | editable: yes
100 | required: no
101 | validation: '/.*/'
102 | validationOptions: []
103 | options: []
104 | arrayToText: no
105 | template: ""
106 | templateUrl: undefined
107 | popoverTemplate: ""
108 | popoverTemplateUrl: undefined
109 | .toEqual component
110 |
111 | it '$builderProvider.convertComponent()', ->
112 | component = builderProvider.convertComponent 'inputText',
113 | group: 'GroupA'
114 | label: 'Input Text'
115 | description: 'description'
116 | placeholder: 'placeholder'
117 | editable: no
118 | required: yes
119 | validation: '/regexp/'
120 | validationOptions: []
121 | options: ['value one']
122 | arrayToText: yes
123 | template: ""
124 | popoverTemplate: ""
125 | expect
126 | name: 'inputText'
127 | group: 'GroupA'
128 | label: 'Input Text'
129 | description: 'description'
130 | placeholder: 'placeholder'
131 | editable: no
132 | required: yes
133 | validation: '/regexp/'
134 | validationOptions: []
135 | options: ['value one']
136 | arrayToText: yes
137 | template: ""
138 | templateUrl: undefined
139 | popoverTemplate: ""
140 | popoverTemplateUrl: undefined
141 | .toEqual component
142 |
143 |
144 | describe '$builderProvider.convertFormObject()', ->
145 | it '$builderProvider.convertFormObject() argument with the not exist component', ->
146 | expect ->
147 | builderProvider.convertFormObject 'default',
148 | component: 'input'
149 | .toThrow()
150 |
151 | it '$builderProvider.convertFormObject() with default value', inject ($builder) ->
152 | # Register the component `inputText`.
153 | $builder.registerComponent 'inputText',
154 | group: 'GroupA'
155 | label: 'Input Text'
156 | description: 'description'
157 | placeholder: 'placeholder'
158 | editable: yes
159 | required: yes
160 | validation: '/regexp/'
161 | options: ['value one']
162 | arrayToText: yes
163 | template: ""
164 | popoverTemplate: ""
165 | formObject = builderProvider.convertFormObject 'default',
166 | component: 'inputText'
167 |
168 | expect
169 | id: undefined
170 | component: 'inputText'
171 | editable: yes
172 | index: 0
173 | label: 'Input Text'
174 | description: 'description'
175 | placeholder: 'placeholder'
176 | options: ['value one']
177 | required: yes
178 | validation: '/regexp/'
179 | .toEqual formObject
180 |
181 | it '$builderProvider.convertFormObject()', inject ($builder) ->
182 | $builder.registerComponent 'inputText',
183 | template: ""
184 | popoverTemplate: ""
185 |
186 | formObject = builderProvider.convertFormObject 'default',
187 | component: 'inputText'
188 | editable: no
189 | label: 'input label'
190 | description: 'description A'
191 | placeholder: 'placeholder A'
192 | options: ['value']
193 | required: no
194 | validation: '/.*/'
195 |
196 | expect
197 | id: undefined
198 | component: 'inputText'
199 | editable: no
200 | index: 0
201 | label: 'input label'
202 | description: 'description A'
203 | placeholder: 'placeholder A'
204 | options: ['value']
205 | required: no
206 | validation: '/.*/'
207 | .toEqual formObject
208 |
209 |
210 | describe '$builderProvider.reindexFormObject()', ->
211 | it '$builderProvider.reindexFormObject()', ->
212 | builderProvider.forms.default.push index: 0
213 | builderProvider.forms.default.push index: 0
214 |
215 | calledCount = jasmine.createSpy 'calledCount'
216 | builderProvider.reindexFormObject 'default'
217 | for index in [0...builderProvider.forms.default.length] by 1
218 | formObject = builderProvider.forms.default[index]
219 | expect(formObject.index).toBe index
220 | calledCount()
221 | expect(calledCount.calls.count()).toBe 2
222 |
223 |
224 | describe '$builder.registerComponent()', ->
225 | it '$builder.registerComponent()', inject ($builder) ->
226 | $builder.registerComponent 'textInput',
227 | group: 'Default'
228 | label: ''
229 | description: ''
230 | placeholder: ''
231 | editable: yes
232 | required: no
233 | validation: '/.*/'
234 | options: []
235 | arrayToText: no
236 | template: ""
237 | popoverTemplate: ""
238 |
239 | expect
240 | name: 'textInput'
241 | group: 'Default'
242 | label: ''
243 | description: ''
244 | placeholder: ''
245 | editable: yes
246 | required: no
247 | validation: '/.*/'
248 | validationOptions: []
249 | options: []
250 | arrayToText: no
251 | template: ""
252 | templateUrl: undefined
253 | popoverTemplate: ""
254 | popoverTemplateUrl: undefined
255 | .toEqual $builder.components.textInput
256 |
257 | it '$builder.registerComponent() the same component will call console.error()', inject ($builder) ->
258 | $builder.registerComponent 'textInput',
259 | template: ""
260 | popoverTemplate: ""
261 | spyOn(console, 'error').and.callFake (msg) ->
262 | expect('The component textInput was registered.').toEqual msg
263 | $builder.registerComponent 'textInput',
264 | template: ""
265 | popoverTemplate: ""
266 | expect(console.error).toHaveBeenCalled()
267 |
268 |
269 | describe '$builder.addFormObject()', ->
270 | beforeEach -> inject ($builder) ->
271 | $builder.registerComponent 'inputText',
272 | template: ""
273 | popoverTemplate: ""
274 |
275 | it '$builder.addFormObject() will call $builderProvider.insertFormObject()', inject ($builder) ->
276 | spyOn(builderProvider, 'insertFormObject').and.callFake (name, index, formObject) ->
277 | expect(name).toEqual 'default'
278 | expect(index).toBe 0
279 | expect
280 | component: 'inputText'
281 | .toEqual formObject
282 | $builder.addFormObject 'default', component: 'inputText'
283 | expect(builderProvider.insertFormObject).toHaveBeenCalled()
284 |
285 | it '$builder.addFormObject() add the form object into the new form', inject ($builder) ->
286 | expect ->
287 | $builder.addFormObject 'new', component: 'inputText'
288 | .not.toThrow()
289 |
290 | describe '$builder.insertFormObject()', ->
291 | beforeEach -> inject ($builder) ->
292 | $builder.registerComponent 'inputText',
293 | template: ""
294 | popoverTemplate: ""
295 |
296 | it '$builder.insertFormObject() index out of bound', inject ($builder) ->
297 | spyOn(builderProvider.forms.default, 'splice').and.callFake (index, length) ->
298 | expect(index).toBe 0
299 | expect(length).toBe 0
300 | $builder.insertFormObject 'default', 1000, component: 'inputText'
301 | expect(builderProvider.forms.default.splice).toHaveBeenCalled()
302 |
303 | it '$builder.insertFormObject() index less than 0', inject ($builder) ->
304 | spyOn(builderProvider.forms.default, 'splice').and.callFake (index, length) ->
305 | expect(index).toBe 0
306 | expect(length).toBe 0
307 | $builder.insertFormObject 'default', -1, component: 'inputText'
308 | expect(builderProvider.forms.default.splice).toHaveBeenCalled()
309 |
310 | it '$builder.insertFormObject()', inject ($builder) ->
311 | $builder.insertFormObject 'default', 0, component: 'inputText'
312 | spyOn(builderProvider.forms.default, 'splice').and.callFake (index, length) ->
313 | expect(index).toBe 1
314 | expect(length).toBe 0
315 | $builder.insertFormObject 'default', 1, component: 'inputText'
316 | expect(builderProvider.forms.default.splice).toHaveBeenCalled()
317 |
318 | it '$builder.insertFormObject() will call convertFormObject() and reindexFormObject()', inject ($builder) ->
319 | spyOn(builderProvider, 'convertFormObject').and.callFake (name, formObject) ->
320 | expect(name).toEqual 'default'
321 | expect
322 | component: 'inputText'
323 | .toEqual formObject
324 | spyOn(builderProvider, 'reindexFormObject').and.callFake (name) ->
325 | expect(name).toEqual 'default'
326 | $builder.insertFormObject 'default', 1, component: 'inputText'
327 | expect(builderProvider.convertFormObject).toHaveBeenCalled()
328 | expect(builderProvider.reindexFormObject).toHaveBeenCalled()
329 |
330 |
331 | describe '$builder.removeFormObject()', ->
332 | beforeEach -> inject ($builder) ->
333 | $builder.registerComponent 'inputText',
334 | template: ""
335 | popoverTemplate: ""
336 |
337 | it '$builder.removeFormObject() will call formObject.splice() and reindexFormObject()', inject ($builder) ->
338 | spyOn(builderProvider.forms.default, 'splice').and.callFake (index, length, object) ->
339 | expect(index).toBe 1
340 | expect(length).toBe 1
341 | expect(object).toBeUndefined()
342 | spyOn(builderProvider, 'reindexFormObject').and.callFake (name) ->
343 | expect(name).toEqual 'default'
344 | $builder.removeFormObject 'default', 1
345 | expect(builderProvider.forms.default.splice).toHaveBeenCalled()
346 | expect(builderProvider.reindexFormObject).toHaveBeenCalled()
347 |
348 |
349 | describe '$builder.updateFormObjectIndex()', ->
350 | beforeEach -> inject ($builder) ->
351 | $builder.registerComponent 'inputText',
352 | template: ""
353 | popoverTemplate: ""
354 |
355 | it '$builder.updateFormObjectIndex() will directive return if the new index and the old index are the same one', inject ($builder) ->
356 | spyOn builderProvider.forms.default, 'splice'
357 | spyOn builderProvider, 'reindexFormObject'
358 | $builder.updateFormObjectIndex 'default', 0, 0
359 | expect(builderProvider.forms.default.splice).not.toHaveBeenCalled()
360 | expect(builderProvider.reindexFormObject).not.toHaveBeenCalled()
361 |
362 | it '$builder.updateFormObjectIndex() will call formObject.splice() and reindexFormObject()', inject ($builder) ->
363 | formObject = id: 0
364 | spySplice = spyOn(builderProvider.forms.default, 'splice').and.callFake (index, length, object) ->
365 | switch spySplice.calls.count()
366 | when 1
367 | expect(index).toBe 0
368 | expect(length).toBe 1
369 | expect(object).toBeUndefined()
370 | [formObject]
371 | when 2
372 | expect(index).toBe 1
373 | expect(length).toBe 0
374 | expect(object).toBe formObject
375 | else
376 | spyOn builderProvider, 'reindexFormObject'
377 | $builder.updateFormObjectIndex 'default', 0, 1
378 | expect(spySplice.calls.count()).toBe 2
379 | expect(builderProvider.reindexFormObject).toHaveBeenCalled()
380 |
381 |
382 | describe '$builder.$get()', ->
383 | it '$builder.config is equal $builderProvider.config', inject ($builder) ->
384 | expect($builder.config).toBe builderProvider.config
385 | it '$builder.components is equal $builderProvider.components', inject ($builder) ->
386 | expect($builder.components).toBe builderProvider.components
387 | it '$builder.groups is equal $builderProvider.groups', inject ($builder) ->
388 | expect($builder.groups).toBe builderProvider.groups
389 | it '$builder.forms is equal $builderProvider.forms', inject ($builder) ->
390 | expect($builder.forms).toBe builderProvider.forms
391 | it '$builder.broadcastChannel is equal $builderProvider.broadcastChannel', inject ($builder) ->
392 | expect($builder.broadcastChannel).toBe builderProvider.broadcastChannel
393 | it '$builder.registerComponent is equal $builderProvider.registerComponent', inject ($builder) ->
394 | expect($builder.registerComponent).toBe builderProvider.registerComponent
395 | it '$builder.addFormObject is equal $builderProvider.addFormObject', inject ($builder) ->
396 | expect($builder.addFormObject).toBe builderProvider.addFormObject
397 | it '$builder.insertFormObject is equal $builderProvider.insertFormObject', inject ($builder) ->
398 | expect($builder.insertFormObject).toBe builderProvider.insertFormObject
399 | it '$builder.removeFormObject is equal $builderProvider.removeFormObject', inject ($builder) ->
400 | expect($builder.removeFormObject).toBe builderProvider.removeFormObject
401 | it '$builder.updateFormObjectIndex is equal $builderProvider.updateFormObjectIndex', inject ($builder) ->
402 | expect($builder.updateFormObjectIndex).toBe builderProvider.updateFormObjectIndex
403 |
--------------------------------------------------------------------------------