Parcourir la source

Prerequisites to the custom fields (and space tabs to regular tabs conversion on some files)

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@3919 a333f486-631f-4898-b8df-5754b55c2be0
glajarige il y a 9 ans
Parent
commit
6356f3145b

+ 282 - 280
js/field_set.js

@@ -1,305 +1,307 @@
-//iTop Form handler
+//iTop Field set
+//Used by itop.form_handler and itop.subform_field to list their fields
 ;
 $(function()
 {
-    // the widget definition, where 'itop' is the namespace,
-    // 'form_handler' the widget name
-    $.widget( 'itop.field_set',
-    {
-        // default options
-        options:
-        {
-            field_identifier_attr: 'data-field-id', // convention: fields are rendered into a div and are identified by this attribute
-            fields_list: null,
-            fields_impacts: {},
-            touched_fields: [],
-            is_valid: true,
-            form_path: '',
-            script_element: null,
-            style_element: null
-        },
+	// the widget definition, where 'itop' is the namespace,
+	// 'field_set' the widget name
+	$.widget( 'itop.field_set',
+	{
+		// default options
+		options:
+		{
+			field_identifier_attr: 'data-field-id', // convention: fields are rendered into a div and are identified by this attribute
+			fields_list: null,
+			fields_impacts: {},
+			touched_fields: [],
+			is_valid: true,
+			form_path: '',
+			script_element: null,
+			style_element: null
+		},
 
-        buildData:
-        {
-            script_code: '',
-            style_code: ''
-        },
+		buildData:
+		{
+			script_code: '',
+			style_code: ''
+		},
 
-        // the constructor
-        _create: function()
-        {
-            var me = this;
-            
-            this.element
-            .addClass('field_set');
-            
-            this.element
-            .bind('field_change', function(event, data){
-                console.log('field_set: field_change');
-                me._onFieldChange(event, data);
-            })
-            .bind('update_form', function(event, data){
-                console.log('field_set: update_form');
-                me._onUpdateForm(event, data);
-            })
-            .bind('get_current_values', function(event, data){
-                console.log('field_set: get_current_values');
-                return me._onGetCurrentValues(event, data);
-            })
-            .bind('validate', function(event, data){
-                if (data === undefined)
-                {
-                    data = {};
-                }
-                console.log('field_set: validate');
-                return me._onValidate(event, data);
-            });
+		// the constructor
+		_create: function()
+		{
+			var me = this;
+			
+			this.element
+			.addClass('field_set');
+			
+			this.element
+			.bind('field_change', function(oEvent, oData){
+				console.log('field_set: field_change');
+				me._onFieldChange(oEvent, oData);
+			})
+			.bind('update_form', function(oEvent, oData){
+				console.log('field_set: update_form');
+				me._onUpdateForm(oEvent, oData);
+			})
+			.bind('get_current_values', function(oEvent, oData){
+				console.log('field_set: get_current_values');
+				return me._onGetCurrentValues(oEvent, oData);
+			})
+			.bind('validate', function(oEvent, oData){
+				if (oData === undefined)
+				{
+					oData = {};
+				}
+				console.log('field_set: validate');
+				return me._onValidate(oEvent, oData);
+			});
 
-            // Creating DOM elements if not using user's specifics
-            if(this.options.script_element === null)
-            {
-                this.options.script_element = $('<script type="text/javascript"></script>');
-                this.element.after(this.options.script_element);
-            }
-            if(this.options.style_element === null)
-            {
-                this.options.style_element = $('<style></style>');
-                this.element.before(this.options.style_element);
-            }
+			// Creating DOM elements if not using user's specifics
+			if(this.options.script_element === null)
+			{
+				this.options.script_element = $('<script type="text/javascript"></script>');
+				this.element.after(this.options.script_element);
+			}
+			if(this.options.style_element === null)
+			{
+				this.options.style_element = $('<style></style>');
+				this.element.before(this.options.style_element);
+			}
 
-            // Building the form
-            if(this.options.fields_list !== null)
-            {
-                this.buildForm();
-            }
-        },
+			// Building the form
+			if(this.options.fields_list !== null)
+			{
+				this.buildForm();
+			}
+		},
    
-        // called when created, and later when changing options
-        _refresh: function()
-        {
-            
-        },
-        // events bound via _bind are removed automatically
-        // revert other modifications here
-        _destroy: function()
-        {
-            this.element
-            .removeClass('field_set');
-        },
-        // _setOptions is called with a hash of all options that are changing
-        // always refresh when changing options
-        _setOptions: function()
-        {
-            this._superApply(arguments);
-        },
-        // _setOption is called for each individual option that is changing
-        _setOption: function( key, value )
-        {
-            this._super( key, value );
-        },
-        _getField: function (sFieldId)
-        {
-            return this.element.find('[' + this.options.field_identifier_attr + '="'+sFieldId+'"][data-form-path="'+this.options.form_path+'"]');
-        },
-        _onGetCurrentValues: function(event, data)
-        {
-            event.stopPropagation();
+		// called when created, and later when changing options
+		_refresh: function()
+		{
+			
+		},
+		// events bound via _bind are removed automatically
+		// revert other modifications here
+		_destroy: function()
+		{
+			this.element
+			.removeClass('field_set');
+		},
+		// _setOptions is called with a hash of all options that are changing
+		// always refresh when changing options
+		_setOptions: function()
+		{
+			this._superApply(arguments);
+		},
+		// _setOption is called for each individual option that is changing
+		_setOption: function( key, value )
+		{
+			this._super( key, value );
+		},
+		getField: function (sFieldId)
+		{
+			return this.element.find('[' + this.options.field_identifier_attr + '="' + sFieldId + '"][data-form-path="' + this.options.form_path + '"]');
+		},
+		_onGetCurrentValues: function(oEvent, oData)
+		{
+			oEvent.stopPropagation();
 
-            var result = {};
-            
-            for(var i in this.options.fields_list)
-            {
-                var field = this.options.fields_list[i];
-                if(this._getField(field.id).hasClass('form_field'))
-                {
-                    result[field.id] = this._getField(field.id).triggerHandler('get_current_value');
-                }
-                else
-                {
-                    console.log('Field set : Cannot retrieve current value from field [' + this.options.field_identifier_attr + '="'+field.id+'"] as it seems to have no itop.form_field widget attached.');
-                }
-            }
-            
-            return result;
-        },
-        _getRequestedFields: function(sourceFieldName)
-        {
-            var fieldsName = [];
-            
-            if(this.options.fields_impacts[sourceFieldName] !== undefined)
-            {
-                for(var i in this.options.fields_impacts[sourceFieldName])
-                {
-                    fieldsName.push(this.options.fields_impacts[sourceFieldName][i]);
-                }
-            }
-            
-            return fieldsName;
-        },
-        _onFieldChange: function(event, data)
-        {
-            event.stopPropagation();
+			var oResult = {};
+			
+			for(var i in this.options.fields_list)
+			{
+				var oField = this.options.fields_list[i];
+				if(this.getField(oField.id).hasClass('form_field'))
+				{
+					oResult[oField.id] = this.getField(oField.id).triggerHandler('get_current_value');
+				}
+				else
+				{
+					console.log('Field set : Cannot retrieve current value from field [' + this.options.field_identifier_attr + '="' + oField.id + '"][data-form-path="' + this.options.form_path + '"] as it seems to have no itop.form_field widget attached.');
+				}
+			}
+			
+			return oResult;
+		},
+		_getRequestedFields: function(sSourceFieldName)
+		{
+			var aFieldsName = [];
+			
+			if(this.options.fields_impacts[sSourceFieldName] !== undefined)
+			{
+				for(var i in this.options.fields_impacts[sSourceFieldName])
+				{
+					aFieldsName.push(this.options.fields_impacts[sSourceFieldName][i]);
+				}
+			}
+			
+			return aFieldsName;
+		},
+		_onFieldChange: function(oEvent, oData)
+		{
+			oEvent.stopPropagation();
 
-            // Set field as touched so we know that we have to do checks on it later
-            if(this.options.touched_fields.indexOf(data.name) < 0)
-            {
-                this.options.touched_fields.push(data.name);
-            }
+			// Set field as touched so we know that we have to do checks on it later
+			if(this.options.touched_fields.indexOf(oData.name) < 0)
+			{
+				this.options.touched_fields.push(oData.name);
+			}
 
-            // Validate the field
-            var oRes = this._getField(data.name).triggerHandler('validate', {touched_fields_only: true});
-            if (!oRes.is_valid)
-            {
-                this.options.is_valid = false;
-            }
+			// Validate the field
+			var oResult = this.getField(oData.name).triggerHandler('validate', {touched_fields_only: true});
+			if (!oResult.is_valid)
+			{
+				this.options.is_valid = false;
+			}
 
-            var requestedFields = this._getRequestedFields(data.name);
-            if(requestedFields.length > 0)
-            {
-                this.element.trigger('update_fields', {form_path: this.options.form_path, requested_fields: requestedFields});
-            }
-        },
-        _onUpdateForm: function(event, data)
-        {
-            event.stopPropagation();
+			var oRequestedFields = this._getRequestedFields(oData.name);
+			if(oRequestedFields.length > 0)
+			{
+				this.element.trigger('update_fields', {form_path: this.options.form_path, requested_fields: oRequestedFields});
+			}
+		},
+		_onUpdateForm: function(oEvent, oData)
+		{
+			oEvent.stopPropagation();
 
-            this.buildData.script_code = '';
-            this.buildData.style_code = '';
+			this.buildData.script_code = '';
+			this.buildData.style_code = '';
 
-            for (var i in data.updated_fields)
-            {
-                var updated_field = data.updated_fields[i];
-                this.options.fields_list[updated_field.id] = updated_field;
-                this._prepareField(updated_field.id);
-            }
+			for (var i in oData.updated_fields)
+			{
+				var oUpdatedField = oData.updated_fields[i];
+				this.options.fields_list[oUpdatedField.id] = oUpdatedField;
+				this._prepareField(oUpdatedField.id);
+			}
 
-            // Adding code to the dom
-            this.options.script_element.append('\n\n// Appended by update at ' + Date() + '\n' + this.buildData.script_code);
-            this.options.style_element.append('\n\n// Appended by update at ' + Date() + '\n' + this.buildData.style_code);
+			// Adding code to the dom
+			this.options.script_element.append('\n\n// Appended by update on ' + Date() + '\n' + this.buildData.script_code);
+			this.options.style_element.append('\n\n// Appended by update on ' + Date() + '\n' + this.buildData.style_code);
 
-            // Evaluating script code as adding it to dom did not executed it (only script from update !)
-            eval(this.buildData.script_code);
-        },
-        _onValidate: function(event, data)
-        {
-            event.stopPropagation();
+			// Evaluating script code as adding it to dom did not executed it (only script from update !)
+			eval(this.buildData.script_code);
+		},
+		_onValidate: function(oEvent, oData)
+		{
+			oEvent.stopPropagation();
 
-            this.options.is_valid = true;
+			this.options.is_valid = true;
 
+			var aFieldsToValidate = [];
+			if ((oData.touched_fields_only !== undefined) && (oData.touched_fields_only === true))
+			{
+				aFieldsToValidate = this.options.touched_fields;
+			}
+			else
+			{
+				// TODO : Requires IE9+ Object.keys(this.options.fields_list);
+				for (var sFieldId in this.options.fields_list)
+				{
+					aFieldsToValidate.push(sFieldId);
+				}
+			}
 
-            var aFieldsToValidate = [];
-            if ((data.touched_fields_only !== undefined) && (data.touched_fields_only === true))
-            {
-                aFieldsToValidate = this.options.touched_fields;
-            }
-            else
-            {
-                // Requires IE9+ Object.keys(this.options.fields_list);
-                for (var sFieldId in this.options.fields_list)
-                {
-                    aFieldsToValidate.push(sFieldId);
-                }
-            }
+			for(var i in aFieldsToValidate)
+			{
+				var oRes = this.getField(aFieldsToValidate[i]).triggerHandler('validate', oData);
+				if (!oRes.is_valid)
+				{
+					this.options.is_valid = false;
+				}
+			}
+			return this.options.is_valid;
+		},
+		// Debug helper
+		showOptions: function()
+		{
+			return this.options;
+		},
+		_loadCssFile: function(url)
+		{
+			if (!$('link[href="' + url + '"]').length)
+				$('<link href="' + url + '" rel="stylesheet">').appendTo('head');
+		},
+		_loadJsFile: function(url)
+		{
+			if (!$('script[src="' + url + '"]').length)
+				$.getScript(url);
+		},
+		// Place a field for which no container exists
+		_addField: function(sFieldId)
+		{
+			$('<div ' + this.options.field_identifier_attr + '="' + sFieldId + '" data-form-path="' + this.options.form_path + '"></div>').appendTo(this.element);
+		},
+		_prepareField: function(sFieldId)
+		{
+			var oField = this.options.fields_list[sFieldId];
 
-            for(var i in aFieldsToValidate)
-            {
-                var oRes = this._getField(aFieldsToValidate[i]).triggerHandler('validate', data);
-                if (!oRes.is_valid)
-                {
-                    this.options.is_valid = false;
-                }
-            }
-            return this.options.is_valid;
-        },
-        showOptions: function() // Debug helper
-        {
-            console.log(this.options);
-            return this.options;
-        },
-        _loadCssFile: function(url)
-        {
-            if (!$('link[href="'+url+'"]').length)
-                $('<link href="'+url+'" rel="stylesheet">').appendTo('head');
-        },
-        _loadJsFile: function(url)
-        {
-            if (!$('script[src="'+url+'"]').length)
-                $.getScript(url);
-        },
-        // Place a field for which no container exists
-        _addField: function(field_id)
-        {
-            $('<div ' + this.options.field_identifier_attr + '="'+field_id+'" data-form-path="' + this.options.form_path + '"></div>').appendTo(this.element);
-        },
-        _prepareField: function(field_id)
-        {
-            var field = this.options.fields_list[field_id];
+			if(this.getField(oField.id).length === 1)
+			{
+				// We replace the node instead of just replacing the inner html so the previous widget is automatically destroyed.
+				this.getField(oField.id).replaceWith(
+					$('<div ' + this.options.field_identifier_attr + '="' + oField.id + '" data-form-path="' + this.options.form_path + '"></div>')
+				);
+			}
+			else
+			{
+				this._addField(oField.id);
+			}
 
-            if(this._getField(field.id).length === 1)
-            {
-                // We replace the node instead of just replacing the inner html so the previous widget is automatically destroyed.
-                this._getField(field.id).replaceWith( $('<div ' + this.options.field_identifier_attr + '="'+field.id+'" data-form-path="' + this.options.form_path + '"></div>') );
-            }
-            else
-            {
-                this._addField(field.id);
-            }
+			var oFieldContainer = this.getField(oField.id);
+			// HTML
+			if( (oField.html !== undefined) && (oField.html !== '') )
+			{
+				oFieldContainer.html(oField.html);
+			}
+			// JS files
+			if( (oField.js_files !== undefined) && (oField.js_files.length > 0) )
+			{
+				for(var j in oField.js_files)
+				{
+					this._loadJsFile(oField.js_files[i]);
+				}
+			}
+			// CSS files
+			if( (oField.css_files !== undefined) && (oField.css_files.length > 0) )
+			{
+				for(var j in oField.css_files)
+				{
+					this._loadCssFile(oField.css_files[i]);
+				}
+			}
+			// JS inline
+			if( (oField.js_inline !== undefined) && (oField.js_inline !== '') )
+			{
+				this.buildData.script_code += '; '+ oField.js_inline;
+			}
+			// CSS inline
+			if( (oField.css_inline !== undefined) && (oField.css_inline !== '') )
+			{
+				this.buildData.style_code += ' '+ oField.css_inline;
+			}
+			
+		},
+		buildForm: function()
+		{
+			this.buildData.script_code = '';
+			this.buildData.style_code = '';
 
-            var field_container = this._getField(field.id);
-            // HTML
-            if( (field.html !== undefined) && (field.html !== '') )
-            {
-                field_container.html(field.html);
-            }
-            // JS files
-            if( (field.js_files !== undefined) && (field.js_files.length > 0) )
-            {
-                for(var j in field.js_files)
-                {
-                    this._loadJsFile(field.js_files[i]);
-                }
-            }
-            // CSS files
-            if( (field.css_files !== undefined) && (field.css_files.length > 0) )
-            {
-                for(var j in field.css_files)
-                {
-                    this._loadCssFile(field.css_files[i]);
-                }
-            }
-            // JS inline
-            if( (field.js_inline !== undefined) && (field.js_inline !== '') )
-            {
-                this.buildData.script_code += '; '+ field.js_inline;
-            }
-            // CSS inline
-            if( (field.css_inline !== undefined) && (field.css_inline !== '') )
-            {
-                this.buildData.style_code += ' '+ field.css_inline;
-            }
-            
-        },
-        buildForm: function()
-        {
-            this.buildData.script_code = '';
-            this.buildData.style_code = '';
+			for(var i in this.options.fields_list)
+			{
+				var oField = this.options.fields_list[i];
+				if(oField.id === undefined)
+				{
+					console.log('Field set : An field must have at least an id property.');
+					return false;
+				}
 
-            for(var i in this.options.fields_list)
-            {
-                var field = this.options.fields_list[i];
-                if(field.id === undefined)
-                {
-                    console.log('Field set : An field must have at least an id property.');
-                    return false;
-                }
+				this._prepareField(oField.id);
+			}
 
-                this._prepareField(field.id);
-            }
-
-            this.options.script_element.text('$(document).ready(function(){ '+this.buildData.script_code+' });');
-            this.options.style_element.text(this.buildData.style_code);
-            
-            eval(this.options.script_element.text());
-        }
-    });
+			this.options.script_element.text('$(document).ready(function(){ ' + this.buildData.script_code + ' });');
+			this.options.style_element.text(this.buildData.style_code);
+			
+			eval(this.options.script_element.text());
+		}
+	});
 });

+ 198 - 196
js/form_field.js

@@ -2,203 +2,205 @@
 ;
 $(function()
 {
-    // the widget definition, where 'itop' is the namespace,
-    // 'form_field' the widget name
-    $.widget( 'itop.form_field',
-    {
-        // default options
-        options:
-        {
-            validators: null,
-            validate_callback: 'validate',                  // When using an anonymous function, use the 'me' parameter to acces the current widget : function(me){ return me.validate(); },
-            on_validation_callback: function(data){  },
-            get_current_value_callback: 'getCurrentValue',
-            
-        },
+	// the widget definition, where 'itop' is the namespace,
+	// 'form_field' the widget name
+	$.widget( 'itop.form_field',
+	{
+		// default options
+		options:
+		{
+			validators: null,
+			validate_callback: 'validate',				  // When using an anonymous function, use the 'me' parameter to acces the current widget : function(me){ return me.validate(); },
+			on_validation_callback: function(data){  },
+			get_current_value_callback: 'getCurrentValue',
+			
+		},
    
-        // the constructor
-        _create: function()
-        {
-            var me = this;
-            
-            this.element
-            .addClass('form_field');
-           
-            this.element
-            .bind('set_validators', function(event, data){
-                event.stopPropagation();
-                me.options.validators = data;
-            });
-            this.element
-            .bind('validate get_current_value', function(event, data){
-                event.stopPropagation();
-                var callback = me.options[event.type+'_callback'];
-                
-                if(typeof callback === 'string')
-                {
-                    return me[callback](event, data);
-                }
-                else if(typeof callback === 'function')
-                {
-                    return callback(me, event, data);
-                }
-                else
-                {
-                    console.log('Form field : callback type must be a function or a existing function name of the widget');
-                    return false;
-                }
-            });
-        },
-        // called when created, and later when changing options
-        _refresh: function()
-        {
+		// the constructor
+		_create: function()
+		{
+			var me = this;
+			
+			this.element
+			.addClass('form_field');
+		   
+			this.element
+			.bind('set_validators', function(oEvent, oData){
+				oEvent.stopPropagation();
+				me.options.validators = oData;
+			});
+			this.element
+			.bind('validate get_current_value', function(oEvent, oData){
+				oEvent.stopPropagation();
+		
+				var callback = me.options[oEvent.type+'_callback'];
+				
+				if(typeof callback === 'string')
+				{
+					return me[callback](oEvent, oData);
+				}
+				else if(typeof callback === 'function')
+				{
+					return callback(me, oEvent, oData);
+				}
+				else
+				{
+					console.log('Form field : callback type must be a function or a existing function name of the widget');
+					return false;
+				}
+			});
+		},
+		// called when created, and later when changing options
+		_refresh: function()
+		{
 
-        },
-        // events bound via _bind are removed automatically
-        // revert other modifications here
-        _destroy: function()
-        {
-            this.element
-            .removeClass('form_field');
-        },
-        // _setOptions is called with a hash of all options that are changing
-        // always refresh when changing options
-        _setOptions: function()
-        {
-            this._superApply(arguments);
-        },
-        // _setOption is called for each individual option that is changing
-        _setOption: function( key, value )
-        {
-            this._super( key, value );
-        },
-        getCurrentValue: function()
-        {
-            var value = null;
-            
-            this.element.find(':input').each(function(index, elem){
-                if($(elem).is(':hidden') || $(elem).is(':text') || $(elem).is('textarea'))
-                {
-                    value = $(elem).val();
-                }
-                else if($(elem).is('select'))
-                {
-                    value = [];
-                    $(elem).find('option:selected').each(function(){
-                        value.push($(this).val());
-                    });
-                }
-                else if($(elem).is(':checkbox') || $(elem).is(':radio'))
-                {
-                    if(value === null)
-                    {
-                        value = [];
-                    }
-                    if($(elem).is(':checked'))
-                    {
-                        value.push($(elem).val());
-                    }
-                }
-                else
-                {
-                    console.log('Form field : Input type not handle yet.');
-                }
-            });
-            
-            return value;
-        },
-        validate: function(event, data)
-        {
-            var oResult = { is_valid: true, error_messages: [] };
+		},
+		// events bound via _bind are removed automatically
+		// revert other modifications here
+		_destroy: function()
+		{
+			this.element
+			.removeClass('form_field');
+		},
+		// _setOptions is called with a hash of all options that are changing
+		// always refresh when changing options
+		_setOptions: function()
+		{
+			this._superApply(arguments);
+		},
+		// _setOption is called for each individual option that is changing
+		_setOption: function( key, value )
+		{
+			this._super( key, value );
+		},
+		getCurrentValue: function()
+		{
+			var value = null;
+			
+			this.element.find(':input').each(function(iIndex, oElem){
+				if($(oElem).is(':hidden') || $(oElem).is(':text') || $(oElem).is('textarea'))
+				{
+					value = $(oElem).val();
+				}
+				else if($(oElem).is('select'))
+				{
+					value = [];
+					$(oElem).find('option:selected').each(function(){
+						value.push($(this).val());
+					});
+				}
+				else if($(oElem).is(':checkbox') || $(oElem).is(':radio'))
+				{
+					if(value === null)
+					{
+						value = [];
+					}
+					if($(oElem).is(':checked'))
+					{
+						value.push($(oElem).val());
+					}
+				}
+				else
+				{
+					console.log('Form field : Input type not handle yet.');
+				}
+			});
+			
+			return value;
+		},
+		validate: function(oEvent, oData)
+		{
+			var oResult = { is_valid: true, error_messages: [] };
 
-            // Doing data validation
-            if(this.options.validators !== null)
-            {
-                var bMandatory = (this.options.validators.mandatory !== undefined);
-                // Extracting value for the field
-                var oValue = this.getCurrentValue();
-                var aValueKeys = Object.keys(oValue);
-                
-                // This is just a safety check in case a field doesn't always return an object when no value assigned, so we have to check the mandatory validator here...
-                // ... But this should never happen.
-                if( (aValueKeys.length === 0) && bMandatory )
-                {
-                    oResult.is_valid = false;
-                    oResult.error_messages.push(this.options.validators.mandatory.message);
-                }
-                // ... Otherwise, we check every validators
-                else if(aValueKeys.length > 0)
-                {
-                    var value = oValue[aValueKeys[0]];
-                    for(var sValidatorType in this.options.validators)
-                    {
-                        var oValidator = this.options.validators[sValidatorType];
-                        if(sValidatorType === 'mandatory')
-                        {
-                            // Works for string, array, object
-                            if($.isEmptyObject(value))
-                            {
-                                oResult.is_valid = false;
-                                oResult.error_messages.push(oValidator.message);
-                            }
-                            // ... In case of non empty array, we have to check if the value is not null
-                            else if($.isArray(value))
-                            {
-                                for(var i in value)
-                                {
-                                    if(typeof value[i] === 'string')
-                                    {
-                                        if($.isEmptyObject(value[i]))
-                                        {
-                                            oResult.is_valid = false;
-                                            oResult.error_messages.push(oValidator.message);
-                                        }
-                                    }
-                                    else
-                                    {
-                                        console.log('Form field: mandatory validation not supported yet for the type "' + (typeof value[i]) +'"');
-                                    }
-                                }
-                            }
-                        }
-                        else
-                        {
-                            var oRegExp = new RegExp(oValidator.reg_exp, "g");
-                            if(typeof value === 'string')
-                            {
-                                if(!oRegExp.test(value))
-                                {
-                                    oResult.is_valid = false;
-                                    oResult.error_messages.push(oValidator.message);
-                                }
-                            }
-                            else if($.isArray(value))
-                            {
-                                for(var i in value)
-                                {
-                                    if(value[i] === 'string' && !oRegExp.test(value))
-                                    {
-                                        oResult.is_valid = false;
-                                        oResult.error_messages.push(oValidator.message);
-                                    }
-                                }
-                            }
-                            else
-                            {
-                                console.log('Form field: validation not supported yet for the type "' + (typeof value) +'"');
-                            }
-                        }
-                    }
-                }
-            }
-            
-            this.options.on_validation_callback(this, oResult);
-            
-            return oResult;
-        },
-        showOptions: function()
-        {
-            return this.options;
-        }
-    });
+			// Doing data validation
+			if(this.options.validators !== null)
+			{
+				var bMandatory = (this.options.validators.mandatory !== undefined);
+				// Extracting value for the field
+				var oValue = this.getCurrentValue();
+				var aValueKeys = Object.keys(oValue);
+				
+				// This is just a safety check in case a field doesn't always return an object when no value assigned, so we have to check the mandatory validator here...
+				// ... But this should never happen.
+				if( (aValueKeys.length === 0) && bMandatory )
+				{
+					oResult.is_valid = false;
+					oResult.error_messages.push(this.options.validators.mandatory.message);
+				}
+				// ... Otherwise, we check every validators
+				else if(aValueKeys.length > 0)
+				{
+					var value = oValue[aValueKeys[0]];
+					for(var sValidatorType in this.options.validators)
+					{
+						var oValidator = this.options.validators[sValidatorType];
+						if(sValidatorType === 'mandatory')
+						{
+							// Works for string, array, object
+							if($.isEmptyObject(value))
+							{
+								oResult.is_valid = false;
+								oResult.error_messages.push(oValidator.message);
+							}
+							// ... In case of non empty array, we have to check if the value is not null
+							else if($.isArray(value))
+							{
+								for(var i in value)
+								{
+									if(typeof value[i] === 'string')
+									{
+										if($.isEmptyObject(value[i]))
+										{
+											oResult.is_valid = false;
+											oResult.error_messages.push(oValidator.message);
+										}
+									}
+									else
+									{
+										console.log('Form field: mandatory validation not supported yet for the type "' + (typeof value[i]) +'"');
+									}
+								}
+							}
+						}
+						else
+						{
+							var oRegExp = new RegExp(oValidator.reg_exp, "g");
+							if(typeof value === 'string')
+							{
+								if(!oRegExp.test(value))
+								{
+									oResult.is_valid = false;
+									oResult.error_messages.push(oValidator.message);
+								}
+							}
+							else if($.isArray(value))
+							{
+								for(var i in value)
+								{
+									if(value[i] === 'string' && !oRegExp.test(value))
+									{
+										oResult.is_valid = false;
+										oResult.error_messages.push(oValidator.message);
+									}
+								}
+							}
+							else
+							{
+								console.log('Form field: validation not supported yet for the type "' + (typeof value) +'"');
+							}
+						}
+					}
+				}
+			}
+			
+			this.options.on_validation_callback(this, oResult);
+			
+			return oResult;
+		},
+		// Debug helper
+		showOptions: function()
+		{
+			return this.options;
+		}
+	});
 });

+ 142 - 141
js/form_handler.js

@@ -2,150 +2,151 @@
 ;
 $(function()
 {
-    // the widget definition, where 'itop' is the namespace,
-    // 'form_handler' the widget name
-    $.widget( 'itop.form_handler',
-    {
-        // default options
-        options:
-        {
-            formmanager_class: null,
-            formmanager_data: null,
-            submit_btn_selector: null,
-            cancel_btn_selector: null,
-            endpoint: null,
-            is_modal: false,
-            field_set: null
-        },
+	// the widget definition, where 'itop' is the namespace,
+	// 'form_handler' the widget name
+	$.widget( 'itop.form_handler',
+	{
+		// default options
+		options:
+		{
+			formmanager_class: null,
+			formmanager_data: null,
+			submit_btn_selector: null,
+			cancel_btn_selector: null,
+			endpoint: null,
+			is_modal: false,
+			field_set: null
+		},
 
-        // the constructor
-        _create: function()
-        {
-            var me = this;
-            
-            this.element
-            .addClass('form_handler');
+		// the constructor
+		_create: function()
+		{
+			var me = this;
+			
+			this.element
+			.addClass('form_handler');
 
-            this.element.bind('update_fields', function(event, data){
-                this._onUpdateFields(event, data);
-            });
+			// Binding events
+			this.element.bind('update_fields', function(oEvent, oData){
+				me._onUpdateFields(oEvent, oData);
+			});
 
-            // Binding buttons
-            if(this.options.submit_btn_selector !== null)
-            {
-                this.options.submit_btn_selector.off('click').on('click', function(event){ me._onSubmitClick(event); });
-            }
-            if(this.options.cancel_btn_selector !== null)
-            {
-                this.options.cancel_btn_selector.off('click').on('click', function(event){ me._onCancelClick(event); });
-            }
-        },
+			// Binding buttons
+			if(this.options.submit_btn_selector !== null)
+			{
+				this.options.submit_btn_selector.off('click').on('click', function(oEvent){ me._onSubmitClick(oEvent); });
+			}
+			if(this.options.cancel_btn_selector !== null)
+			{
+				this.options.cancel_btn_selector.off('click').on('click', function(oEvent){ me._onCancelClick(oEvent); });
+			}
+		},
    
-        // called when created, and later when changing options
-        _refresh: function()
-        {
-            
-        },
-        // events bound via _bind are removed automatically
-        // revert other modifications here
-        _destroy: function()
-        {
-            this.element
-            .removeClass('form_handler');
-        },
-        // _setOptions is called with a hash of all options that are changing
-        // always refresh when changing options
-        _setOptions: function()
-        {
-            this._superApply(arguments);
-        },
-        // _setOption is called for each individual option that is changing
-        _setOption: function( key, value )
-        {
-            this._super( key, value );
-        },
-        getCurrentValues: function()
-        {
-            return this.options.field_set.triggerHandler('get_current_values');
-        },
-        _onUpdateFields: function(event, data)
-        {
-            var me = this;
-            var sFormPath = data.form_path;
+		// called when created, and later when changing options
+		_refresh: function()
+		{
+			
+		},
+		// events bound via _bind are removed automatically
+		// revert other modifications here
+		_destroy: function()
+		{
+			this.element
+			.removeClass('form_handler');
+		},
+		// _setOptions is called with a hash of all options that are changing
+		// always refresh when changing options
+		_setOptions: function()
+		{
+			this._superApply(arguments);
+		},
+		// _setOption is called for each individual option that is changing
+		_setOption: function( key, value )
+		{
+			this._super( key, value );
+		},
+		getCurrentValues: function()
+		{
+			return this.options.field_set.triggerHandler('get_current_values');
+		},
+		_onUpdateFields: function(oEvent, oData)
+		{
+			var me = this;
+			var sFormPath = oData.form_path;
 
-            // Data checks
-            if(this.options.endpoint === null)
-            {
-                console.log('Form handler : An endpoint must be defined.');
-                return false;
-            }
-            if(this.options.formmanager_class === null)
-            {
-                console.log('Form handler : Form manager class must be defined.');
-                return false;
-            }
-            if(this.options.formmanager_data === null)
-            {
-                console.log('Form handler : Form manager data must be defined.');
-                return false;
-            }
-            
-            this._disableFormBeforeLoading();
-            $.post(
-                this.options.endpoint,
-                {
-                    operation: 'update',
-                    formmanager_class: this.options.formmanager_class,
-                    formmanager_data: JSON.stringify(this.options.formmanager_data),
-                    current_values: this.getCurrentValues(),
-                    requested_fields: data.requested_fields,
-                    form_path: sFormPath
-                },
-                function(data){
-                    me._onUpdateSuccess(data, sFormPath);
-                }
-            )
-            .fail(function(data){ me._onUpdateFailure(data, sFormPath); })
-            .always(function(data){ me._onUpdateAlways(data, sFormPath); });
-        },
-        // Intended for overloading in derived classes
-        _onSubmitClick: function(event)
-        {
-        },
-        // Intended for overloading in derived classes
-        _onCancelClick: function(event)
-        {
-        },
-        // Intended for overloading in derived classes
-        _onUpdateSuccess: function(data, sFormPath)
-        {
-            if(data.form.updated_fields !== undefined)
-            {
-                this.element.find('[data-form-path="'+sFormPath+'"]').trigger('update_form', {updated_fields: data.form.updated_fields});
-            }
-        },
-        // Intended for overloading in derived classes
-        _onUpdateFailure: function(data, sFormPath)
-        {
-        },
-        // Intended for overloading in derived classes
-        _onUpdateAlways: function(data, sFormPath)
-        {
-            // Check all touched AFTER ajax is complete, otherwise the renderer will redraw the field in the mean time.
-            this.element.find('[data-form-path="'+sFormPath+'"]').trigger('validate');
-            this._enableFormAfterLoading();
-        },
-        // Intended for overloading in derived classes
-        _disableFormBeforeLoading: function()
-        {
-        },
-        // Intended for overloading in derived classes
-        _enableFormAfterLoading: function()
-        {
-        },
-        showOptions: function() // Debug helper
-        {
-            console.log(this.options);
-        }
-    });
+			// Data checks
+			if(this.options.endpoint === null)
+			{
+				console.log('Form handler : An endpoint must be defined.');
+				return false;
+			}
+			if(this.options.formmanager_class === null)
+			{
+				console.log('Form handler : Form manager class must be defined.');
+				return false;
+			}
+			if(this.options.formmanager_data === null)
+			{
+				console.log('Form handler : Form manager data must be defined.');
+				return false;
+			}
+			
+			this._disableFormBeforeLoading();
+			$.post(
+				this.options.endpoint,
+				{
+					operation: 'update',
+					formmanager_class: this.options.formmanager_class,
+					formmanager_data: JSON.stringify(this.options.formmanager_data),
+					current_values: this.getCurrentValues(),
+					requested_fields: oData.requested_fields,
+					form_path: sFormPath
+				},
+				function(oData){
+					me._onUpdateSuccess(oData, sFormPath);
+				}
+			)
+			.fail(function(oData){ me._onUpdateFailure(oData, sFormPath); })
+			.always(function(oData){ me._onUpdateAlways(oData, sFormPath); });
+		},
+		// Intended for overloading in derived classes
+		_onSubmitClick: function(oEvent)
+		{
+		},
+		// Intended for overloading in derived classes
+		_onCancelClick: function(oEvent)
+		{
+		},
+		// Intended for overloading in derived classes
+		_onUpdateSuccess: function(oData, sFormPath)
+		{
+			if(oData.form.updated_fields !== undefined)
+			{
+				this.element.find('[data-form-path="' + sFormPath + '"]').trigger('update_form', {updated_fields: oData.form.updated_fields});
+			}
+		},
+		// Intended for overloading in derived classes
+		_onUpdateFailure: function(oData, sFormPath)
+		{
+		},
+		// Intended for overloading in derived classes
+		_onUpdateAlways: function(oData, sFormPath)
+		{
+			// Check all touched AFTER ajax is complete, otherwise the renderer will redraw the field in the mean time.
+			this.element.find('[data-form-path="' + sFormPath + '"]').trigger('validate');
+			this._enableFormAfterLoading();
+		},
+		// Intended for overloading in derived classes
+		_disableFormBeforeLoading: function()
+		{
+		},
+		// Intended for overloading in derived classes
+		_enableFormAfterLoading: function()
+		{
+		},
+		showOptions: function() // Debug helper
+		{
+			console.log(this.options);
+		}
+	});
 });

+ 39 - 39
js/subform_field.js

@@ -1,46 +1,46 @@
-//iTop Form field
+//iTop Subform field
 ;
 $(function()
 {
-    // the widget definition, where 'itop' is the namespace,
-    // 'subform_field' the widget name
-    $.widget( 'itop.subform_field', $.itop.form_field,
-    {
-        // default options
-        options:
-        {
-            field_set: null
-        },
+	// the widget definition, where 'itop' is the namespace,
+	// 'subform_field' the widget name
+	$.widget( 'itop.subform_field', $.itop.form_field,
+	{
+		// default options
+		options:
+		{
+			field_set: null
+		},
    
-        // the constructor
-        _create: function()
-        {
-            var me = this;
-            
-            this.element
-            .addClass('subform_field');
+		// the constructor
+		_create: function()
+		{
+			var me = this;
+			
+			this.element
+			.addClass('subform_field');
 
-            this._super();
-        },
-        // events bound via _bind are removed automatically
-        // revert other modifications here
-        _destroy: function()
-        {
-            this.element
-            .removeClass('subform_field');
+			this._super();
+		},
+		// events bound via _bind are removed automatically
+		// revert other modifications here
+		_destroy: function()
+		{
+			this.element
+			.removeClass('subform_field');
 
-            this._super();
-        },
-        getCurrentValue: function()
-        {
-            return this.options.field_set.triggerHandler('get_current_values');
-        },
-        validate: function(event, data)
-        {
-            return {
-                is_valid: this.options.field_set.triggerHandler('validate', data),
-                error_messages: []
-            }
-        },
-    });
+			this._super();
+		},
+		getCurrentValue: function()
+		{
+			return this.options.field_set.triggerHandler('get_current_values');
+		},
+		validate: function(oEvent, oData)
+		{
+			return {
+				is_valid: this.options.field_set.triggerHandler('validate', oData),
+				error_messages: []
+			}
+		},
+	});
 });

+ 1 - 1
sources/form/field/checkboxfield.class.inc.php

@@ -28,6 +28,6 @@ use \Combodo\iTop\Form\Field\MultipleChoicesField;
  */
 class CheckboxField extends MultipleChoicesField
 {
-    const DEFAULT_MULTIPLE_VALUES_ENABLED = true;
+	const DEFAULT_MULTIPLE_VALUES_ENABLED = true;
 
 }

+ 324 - 248
sources/form/field/field.class.inc.php

@@ -30,254 +30,330 @@ use \Combodo\iTop\Form\Validator\MandatoryValidator;
  */
 abstract class Field
 {
-    const DEFAULT_LABEL = '';
-    const DEFAULT_READ_ONLY = false;
-    const DEFAULT_MANDATORY = false;
-    const DEFAULT_VALID = true;
-
-    protected $sId;
-    protected $sGlobalId;
-    protected $sFormPath;
-    protected $sLabel;
-    protected $bReadOnly;
-    protected $bMandatory;
-    protected $aValidators;
-    protected $bValid;
-    protected $aErrorMessages;
-    protected $currentValue;
-    protected $onFinalizeCallback;
-
-    /**
-     *
-     * @param Closure $callback (Used in the $oForm->AddField($sId, ..., function() use ($oManager, $oForm, '...') { ... } ); )
-     */
-    public function __construct($sId, Closure $onFinalizeCallback = null)
-    {
-        $this->sId = $sId;
-        $this->sGlobalId = 'field_'.$sId.uniqid();
-        $this->sLabel = static::DEFAULT_LABEL;
-        $this->bReadOnly = static::DEFAULT_READ_ONLY;
-        $this->bMandatory = static::DEFAULT_MANDATORY;
-        $this->aValidators = array();
-        $this->bValid = static::DEFAULT_VALID;
-        $this->aErrorMessages = array();
-        $this->onFinalizeCallback = $onFinalizeCallback;
-    }
+	const DEFAULT_LABEL = '';
+	const DEFAULT_READ_ONLY = false;
+	const DEFAULT_MANDATORY = false;
+	const DEFAULT_VALID = true;
+
+	protected $sId;
+	protected $sGlobalId;
+	protected $sFormPath;
+	protected $sLabel;
+	protected $bReadOnly;
+	protected $bMandatory;
+	protected $aValidators;
+	protected $bValid;
+	protected $aErrorMessages;
+	protected $currentValue;
+	protected $onFinalizeCallback;
 
 	/**
-     * Get the field id within its container form
-     * @return string
-     */
-    public function GetId()
-    {
-        return $this->sId;
-    }
-
-    /**
-     * Get a unique field id within the top level form
-     * @return string
-     */
-    public function GetGlobalId()
-    {
-        return $this->sGlobalId;
-    }
-
-    /**
-     * Get the id of the container form
-     * @return string
-     */
-    public function GetFormPath()
-    {
-        return $this->sFormPath;
-    }
-
-    public function GetLabel()
-    {
-        return $this->sLabel;
-    }
-
-    public function GetReadOnly()
-    {
-        return $this->bReadOnly;
-    }
-
-    public function GetMandatory()
-    {
-        return $this->bMandatory;
-    }
-
-    public function GetValidators()
-    {
-        return $this->aValidators;
-    }
-
-    /**
-     * Returns the current validation state of the field (true|false).
-     * It DOESN'T make the validation, see Validate() instead.
-     *
-     * @return boolean
-     */
-    public function GetValid()
-    {
-        return $this->bValid;
-    }
-
-    public function GetErrorMessages()
-    {
-        return $this->aErrorMessages;
-    }
-
-    public function GetCurrentValue()
-    {
-        return $this->currentValue;
-    }
-
-    public function SetLabel($sLabel)
-    {
-        $this->sLabel = $sLabel;
-        return $this;
-    }
-
-    public function SetReadOnly($bReadOnly)
-    {
-        $this->bReadOnly = $bReadOnly;
-        return $this;
-    }
-
-    public function SetMandatory($bMandatory)
-    {
-        // Before changing the property, we check if it was already mandatory. If not, we had the mandatory validator
-        if ($bMandatory && !$this->bMandatory)
-        {
-            $this->AddValidator(new MandatoryValidator());
-        }
-
-        if (!$bMandatory)
-        {
-            foreach ($this->aValidators as $iKey => $oValue)
-            {
-                if ($oValue::Getname() === MandatoryValidator::GetName())
-                {
-                    unset($this->aValidators[$iKey]);
-                }
-            }
-        }
-
-        $this->bMandatory = $bMandatory;
-        return $this;
-    }
-
-    public function SetValidators($aValidators)
-    {
-        $this->aValidators = $aValidators;
-        return $this;
-    }
-
-    /**
-     * Note : Function is protected as bValid should not be set from outside
-     *
-     * @param boolean $bValid
-     * @return \Combodo\iTop\Form\Field\Field
-     */
-    protected function SetValid($bValid)
-    {
-        $this->bValid = $bValid;
-        return $this;
-    }
-
-    /**
-     * Note : Function is protected as aErrorMessages should not be set from outside
-     *
-     * @param array $aErrorMessages
-     * @return \Combodo\iTop\Form\Field\Field
-     */
-    protected function SetErrorMessages($aErrorMessages)
-    {
-        $this->aErrorMessages = $aErrorMessages;
-        return $this;
-    }
-
-    public function SetCurrentValue($currentValue)
-    {
-        $this->currentValue = $currentValue;
-        return $this;
-    }
-
-    public function SetOnFinalizeCallback(Closure $onFinalizeCallback)
-    {
-        $this->onFinalizeCallback = $onFinalizeCallback;
-        return $this;
-    }
-
-    /**
-     * Called by the form when adding the field
-     */
-    public function SetFormPath($sFormPath)
-    {
-        $this->sFormPath = $sFormPath;
-    }
-
-    public function AddValidator(Validator $oValidator)
-    {
-        $this->aValidators[] = $oValidator;
-        return $this;
-    }
-
-    public function RemoveValidator(Validator $oValidator)
-    {
-        foreach ($this->aValidators as $iKey => $oValue)
-        {
-            if ($oValue === $oValidator)
-            {
-                unset($this->aValidators[$iKey]);
-            }
-        }
-        return $this;
-    }
-
-    /**
-     * Note : Function is protected as aErrorMessages should not be add from outside
-     *
-     * @param string $sErrorMessage
-     * @return \Combodo\iTop\Form\Field\Field
-     */
-    protected function AddErrorMessage($sErrorMessage)
-    {
-        $this->aErrorMessages[] = $sErrorMessage;
-        return $this;
-    }
-
-    /**
-     * Note : Function is protected as aErrorMessages should not be set from outside
-     *
-     * @return \Combodo\iTop\Form\Field\Field
-     */
-    protected function EmptyErrorMessages()
-    {
-        $this->aErrorMessages = array();
-        return $this;
-    }
-
-    public function OnCancel()
-    {
-        // Overload when needed
-    }
-
-    public function OnFinalize()
-    {
-        if ($this->onFinalizeCallback !== null)
-        {
-            // Note : We MUST have a temp variable to call the Closure. otherwise it won't work when the Closure is a class member
-            $callback = $this->onFinalizeCallback;
-            $callback($this);
-        }
-    }
-
-    /**
-     * Checks the validators to see if the field's current value is valid.
-     * Then sets $bValid and $aErrorMessages.
-     *
-     * @return boolean
-     */
-    abstract public function Validate();
+	 * Default constructor
+	 *
+	 * @param string $sId
+	 * @param Closure $onFinalizeCallback (Used in the $oForm->AddField($sId, ..., function() use ($oManager, $oForm, '...') { ... } ); )
+	 */
+	public function __construct($sId, Closure $onFinalizeCallback = null)
+	{
+		$this->sId = $sId;
+		$this->sGlobalId = 'field_' . $sId . '_' . uniqid();
+		$this->sLabel = static::DEFAULT_LABEL;
+		$this->bReadOnly = static::DEFAULT_READ_ONLY;
+		$this->bMandatory = static::DEFAULT_MANDATORY;
+		$this->aValidators = array();
+		$this->bValid = static::DEFAULT_VALID;
+		$this->aErrorMessages = array();
+		$this->onFinalizeCallback = $onFinalizeCallback;
+	}
+
+	/**
+	 * Returns the field id within its container form
+	 *
+	 * @return string
+	 */
+	public function GetId()
+	{
+		return $this->sId;
+	}
+
+	/**
+	 * Returns a unique field id within the top level form
+	 *
+	 * @return string
+	 */
+	public function GetGlobalId()
+	{
+		return $this->sGlobalId;
+	}
+
+	/**
+	 * Returns the id of the container form
+	 *
+	 * @return string
+	 */
+	public function GetFormPath()
+	{
+		return $this->sFormPath;
+	}
+
+	/**
+	 *
+	 * @return string
+	 */
+	public function GetLabel()
+	{
+		return $this->sLabel;
+	}
+
+	/**
+	 *
+	 * @return boolean
+	 */
+	public function GetReadOnly()
+	{
+		return $this->bReadOnly;
+	}
+
+	/**
+	 *
+	 * @return boolean
+	 */
+	public function GetMandatory()
+	{
+		return $this->bMandatory;
+	}
+
+	/**
+	 *
+	 * @return array
+	 */
+	public function GetValidators()
+	{
+		return $this->aValidators;
+	}
+
+	/**
+	 * Returns the current validation state of the field (true|false).
+	 * It DOESN'T make the validation, see Validate() instead.
+	 *
+	 * @return boolean
+	 */
+	public function GetValid()
+	{
+		return $this->bValid;
+	}
+
+	/**
+	 *
+	 * @return array
+	 */
+	public function GetErrorMessages()
+	{
+		return $this->aErrorMessages;
+	}
+
+	/**
+	 *
+	 * @return array
+	 */
+	public function GetCurrentValue()
+	{
+		return $this->currentValue;
+	}
+
+	/**
+	 * Sets the field formpath
+	 * Usually Called by the form when adding the field
+	 *
+	 * @param string $sFormPath
+	 * @return \Combodo\iTop\Form\Field\Field
+	 */
+	public function SetFormPath($sFormPath)
+	{
+		$this->sFormPath = $sFormPath;
+		return $this;
+	}
+
+	/**
+	 *
+	 * @param type $sLabel
+	 * @return \Combodo\iTop\Form\Field\Field
+	 */
+	public function SetLabel($sLabel)
+	{
+		$this->sLabel = $sLabel;
+		return $this;
+	}
+
+	/**
+	 *
+	 * @param boolean $bReadOnly
+	 * @return \Combodo\iTop\Form\Field\Field
+	 */
+	public function SetReadOnly($bReadOnly)
+	{
+		$this->bReadOnly = $bReadOnly;
+		return $this;
+	}
+
+	/**
+	 * Sets if the field is mandatory or not.
+	 * Setting the value will automatically add/remove a MandatoryValidator to the Field
+	 *
+	 * @param boolean $bMandatory
+	 * @return \Combodo\iTop\Form\Field\Field
+	 */
+	public function SetMandatory($bMandatory)
+	{
+		// Before changing the property, we check if it was already mandatory. If not, we had the mandatory validator
+		if ($bMandatory && !$this->bMandatory)
+		{
+			$this->AddValidator(new MandatoryValidator());
+		}
+
+		if (!$bMandatory)
+		{
+			foreach ($this->aValidators as $iKey => $oValue)
+			{
+				if ($oValue::Getname() === MandatoryValidator::GetName())
+				{
+					unset($this->aValidators[$iKey]);
+				}
+			}
+		}
+
+		$this->bMandatory = $bMandatory;
+		return $this;
+	}
+
+	/**
+	 *
+	 * @param array $aValidators
+	 * @return \Combodo\iTop\Form\Field\Field
+	 */
+	public function SetValidators($aValidators)
+	{
+		$this->aValidators = $aValidators;
+		return $this;
+	}
+
+	/**
+	 * Note : Function is protected as bValid should not be set from outside
+	 *
+	 * @param boolean $bValid
+	 * @return \Combodo\iTop\Form\Field\Field
+	 */
+	protected function SetValid($bValid)
+	{
+		$this->bValid = $bValid;
+		return $this;
+	}
+
+	/**
+	 * Note : Function is protected as aErrorMessages should not be set from outside
+	 *
+	 * @param array $aErrorMessages
+	 * @return \Combodo\iTop\Form\Field\Field
+	 */
+	protected function SetErrorMessages($aErrorMessages)
+	{
+		$this->aErrorMessages = $aErrorMessages;
+		return $this;
+	}
+
+	/**
+	 *
+	 * @param mixed $currentValue
+	 * @return \Combodo\iTop\Form\Field\Field
+	 */
+	public function SetCurrentValue($currentValue)
+	{
+		$this->currentValue = $currentValue;
+		return $this;
+	}
+
+	/**
+	 *
+	 * @param Closure $onFinalizeCallback
+	 * @return \Combodo\iTop\Form\Field\Field
+	 */
+	public function SetOnFinalizeCallback(Closure $onFinalizeCallback)
+	{
+		$this->onFinalizeCallback = $onFinalizeCallback;
+		return $this;
+	}
+
+	/**
+	 *
+	 * @param Validator $oValidator
+	 * @return \Combodo\iTop\Form\Field\Field
+	 */
+	public function AddValidator(Validator $oValidator)
+	{
+		$this->aValidators[] = $oValidator;
+		return $this;
+	}
+
+	/**
+	 *
+	 * @param Validator $oValidator
+	 * @return \Combodo\iTop\Form\Field\Field
+	 */
+	public function RemoveValidator(Validator $oValidator)
+	{
+		foreach ($this->aValidators as $iKey => $oValue)
+		{
+			if ($oValue === $oValidator)
+			{
+				unset($this->aValidators[$iKey]);
+			}
+		}
+		return $this;
+	}
+
+	/**
+	 * Note : Function is protected as aErrorMessages should not be add from outside
+	 *
+	 * @param string $sErrorMessage
+	 * @return \Combodo\iTop\Form\Field\Field
+	 */
+	protected function AddErrorMessage($sErrorMessage)
+	{
+		$this->aErrorMessages[] = $sErrorMessage;
+		return $this;
+	}
+
+	/**
+	 * Note : Function is protected as aErrorMessages should not be set from outside
+	 *
+	 * @return \Combodo\iTop\Form\Field\Field
+	 */
+	protected function EmptyErrorMessages()
+	{
+		$this->aErrorMessages = array();
+		return $this;
+	}
+
+	public function OnCancel()
+	{
+		// Overload when needed
+	}
+
+	public function OnFinalize()
+	{
+		if ($this->onFinalizeCallback !== null)
+		{
+			// Note : We MUST have a temp variable to call the Closure. otherwise it won't work when the Closure is a class member
+			$callback = $this->onFinalizeCallback;
+			$callback($this);
+		}
+	}
+
+	/**
+	 * Checks the validators to see if the field's current value is valid.
+	 * Then sets $bValid and $aErrorMessages.
+	 *
+	 * @return boolean
+	 */
+	abstract public function Validate();
 }

+ 141 - 141
sources/form/field/multiplechoicesfield.class.inc.php

@@ -32,146 +32,146 @@ use \Combodo\iTop\Form\Field\Field;
  */
 abstract class MultipleChoicesField extends Field
 {
-    const DEFAULT_MULTIPLE_VALUES_ENABLED = false;
-
-    protected $bMultipleValuesEnabled;
-    protected $aChoices;
-
-    public function __construct($sId, Closure $onFinalizeCallback = null)
-    {
-        parent::__construct($sId, $onFinalizeCallback);
-        $this->bMultipleValuesEnabled = static::DEFAULT_MULTIPLE_VALUES_ENABLED;
-        $this->aChoices = array();
-        $this->currentValue = array();
-    }
-
-    public function GetCurrentValue()
-    {
-        $value = null;
-        if (!empty($this->currentValue))
-        {
-            if ($this->bMultipleValuesEnabled)
-            {
-                $value = $this->currentValue;
-            }
-            else
-            {
-                reset($this->currentValue);
-                $value = current($this->currentValue);
-            }
-        }
-
-        return $value;
-    }
-
-    /**
-     * Sets the current value for the MultipleChoicesField.
-     *
-     * @param mixed $currentValue Can be either an array of values (in case of multiple values) or just a simple value
-     * @return \Combodo\iTop\Form\Field\MultipleChoicesField
-     */
-    public function SetCurrentValue($currentValue)
-    {
-        if (is_array($currentValue))
-        {
-            $this->currentValue = $currentValue;
-        }
-        elseif (is_null($currentValue))
-        {
-            $this->currentValue = array();
-        }
-        else
-        {
-            $this->currentValue = array($currentValue);
-        }
-        return $this;
-    }
-
-    public function GetMultipleValuesEnabled()
-    {
-        return $this->bMultipleValuesEnabled;
-    }
-
-    public function SetMultipleValuesEnabled($bMultipleValuesEnabled)
-    {
-        $this->bMultipleValuesEnabled = $bMultipleValuesEnabled;
-        return $this;
-    }
-
-    public function SetValues($aValues)
-    {
-        $this->currentValue = $aValues;
-        return $this;
-    }
-
-    public function AddValue($value)
-    {
-        $this->currentValue = $value;
-        return $this;
-    }
-
-    public function RemoveValue($value)
-    {
-        if (array_key_exists($value, $this->currentValue))
-        {
-            unset($this->currentValue[$sId]);
-        }
-        return $this;
-    }
-
-    public function IsAmongValues($value)
-    {
-        return in_array($value, $this->currentValue);
-    }
-
-    public function GetChoices()
-    {
-        return $this->aChoices;
-    }
-
-    public function SetChoices($aChoices)
-    {
-        $this->aChoices = $aChoices;
-        return $this;
-    }
-
-    public function AddChoice($sId, $choice = null)
-    {
-        if ($choice === null)
-        {
-            $choice = $sId;
-        }
-        $this->aChoices[$sId] = $choice;
-        return $this;
-    }
-
-    public function RemoveChoice($sId)
-    {
-        if (in_array($sId, $this->aChoices))
-        {
-            unset($this->aChoices[$sId]);
-        }
-        return $this;
-    }
-
-    public function Validate()
-    {
-        $this->SetValid(true);
-        $this->EmptyErrorMessages();
-
-        foreach ($this->GetValidators() as $oValidator)
-        {
-            foreach ($this->currentValue as $value)
-            {
-                if (!preg_match($oValidator->GetRegExp(true), $value))
-                {
-                    $this->SetValid(false);
-                    $this->AddErrorMessage($oValidator->GetErrorMessage());
-                }
-            }
-        }
-
-        return $this->GetValid();
-    }
+	const DEFAULT_MULTIPLE_VALUES_ENABLED = false;
+
+	protected $bMultipleValuesEnabled;
+	protected $aChoices;
+
+	public function __construct($sId, Closure $onFinalizeCallback = null)
+	{
+		parent::__construct($sId, $onFinalizeCallback);
+		$this->bMultipleValuesEnabled = static::DEFAULT_MULTIPLE_VALUES_ENABLED;
+		$this->aChoices = array();
+		$this->currentValue = array();
+	}
+
+	public function GetCurrentValue()
+	{
+		$value = null;
+		if (!empty($this->currentValue))
+		{
+			if ($this->bMultipleValuesEnabled)
+			{
+				$value = $this->currentValue;
+			}
+			else
+			{
+				reset($this->currentValue);
+				$value = current($this->currentValue);
+			}
+		}
+
+		return $value;
+	}
+
+	/**
+	 * Sets the current value for the MultipleChoicesField.
+	 *
+	 * @param mixed $currentValue Can be either an array of values (in case of multiple values) or just a simple value
+	 * @return \Combodo\iTop\Form\Field\MultipleChoicesField
+	 */
+	public function SetCurrentValue($currentValue)
+	{
+		if (is_array($currentValue))
+		{
+			$this->currentValue = $currentValue;
+		}
+		elseif (is_null($currentValue))
+		{
+			$this->currentValue = array();
+		}
+		else
+		{
+			$this->currentValue = array($currentValue);
+		}
+		return $this;
+	}
+
+	public function GetMultipleValuesEnabled()
+	{
+		return $this->bMultipleValuesEnabled;
+	}
+
+	public function SetMultipleValuesEnabled($bMultipleValuesEnabled)
+	{
+		$this->bMultipleValuesEnabled = $bMultipleValuesEnabled;
+		return $this;
+	}
+
+	public function SetValues($aValues)
+	{
+		$this->currentValue = $aValues;
+		return $this;
+	}
+
+	public function AddValue($value)
+	{
+		$this->currentValue = $value;
+		return $this;
+	}
+
+	public function RemoveValue($value)
+	{
+		if (array_key_exists($value, $this->currentValue))
+		{
+			unset($this->currentValue[$sId]);
+		}
+		return $this;
+	}
+
+	public function IsAmongValues($value)
+	{
+		return in_array($value, $this->currentValue);
+	}
+
+	public function GetChoices()
+	{
+		return $this->aChoices;
+	}
+
+	public function SetChoices($aChoices)
+	{
+		$this->aChoices = $aChoices;
+		return $this;
+	}
+
+	public function AddChoice($sId, $choice = null)
+	{
+		if ($choice === null)
+		{
+			$choice = $sId;
+		}
+		$this->aChoices[$sId] = $choice;
+		return $this;
+	}
+
+	public function RemoveChoice($sId)
+	{
+		if (in_array($sId, $this->aChoices))
+		{
+			unset($this->aChoices[$sId]);
+		}
+		return $this;
+	}
+
+	public function Validate()
+	{
+		$this->SetValid(true);
+		$this->EmptyErrorMessages();
+
+		foreach ($this->GetValidators() as $oValidator)
+		{
+			foreach ($this->currentValue as $value)
+			{
+				if (!preg_match($oValidator->GetRegExp(true), $value))
+				{
+					$this->SetValid(false);
+					$this->AddErrorMessage($oValidator->GetErrorMessage());
+				}
+			}
+		}
+
+		return $this->GetValid();
+	}
 
 }

+ 1 - 1
sources/form/field/radiofield.class.inc.php

@@ -28,6 +28,6 @@ use \Combodo\iTop\Form\Field\MultipleChoicesField;
  */
 class RadioField extends MultipleChoicesField
 {
-    const DEFAULT_MULTIPLE_VALUES_ENABLED = false;
+	const DEFAULT_MULTIPLE_VALUES_ENABLED = false;
 
 }

+ 43 - 43
sources/form/field/selectfield.class.inc.php

@@ -29,56 +29,56 @@ use \Combodo\iTop\Form\Field\MultipleChoicesField;
  */
 class SelectField extends MultipleChoicesField
 {
-    const DEFAULT_NULL_CHOICE_LABEL = 'TOTR: - Choisir une valeur -';
-    const DEFAULT_STARTS_WITH_NULL_CHOICE = true;
+	const DEFAULT_NULL_CHOICE_LABEL = 'TOTR: - Choisir une valeur -';
+	const DEFAULT_STARTS_WITH_NULL_CHOICE = true;
 
-    protected $bStartsWithNullChoice;
+	protected $bStartsWithNullChoice;
 
-    public function __construct($sId, Closure $onFinalizeCallback = null)
-    {
-        parent::__construct($sId, $onFinalizeCallback);
-        $this->bStartsWithNullChoice = static::DEFAULT_STARTS_WITH_NULL_CHOICE;
-    }
+	public function __construct($sId, Closure $onFinalizeCallback = null)
+	{
+		parent::__construct($sId, $onFinalizeCallback);
+		$this->bStartsWithNullChoice = static::DEFAULT_STARTS_WITH_NULL_CHOICE;
+	}
 
-    /**
-     * Returns if the select starts with a dummy choice before its choices.
-     * This can be useful when you want to force the user to explicitly select a choice.
-     *
-     * @return boolean
-     */
-    public function GetStartsWithNullChoice()
-    {
-        return $this->bStartsWithNullChoice;
-    }
+	/**
+	 * Returns if the select starts with a dummy choice before its choices.
+	 * This can be useful when you want to force the user to explicitly select a choice.
+	 *
+	 * @return boolean
+	 */
+	public function GetStartsWithNullChoice()
+	{
+		return $this->bStartsWithNullChoice;
+	}
 
-    public function SetStartsWithNullChoice($bStartsWithNullChoice)
-    {
-        $this->bStartsWithNullChoice = $bStartsWithNullChoice;
+	public function SetStartsWithNullChoice($bStartsWithNullChoice)
+	{
+		$this->bStartsWithNullChoice = $bStartsWithNullChoice;
 
-        if (!array_key_exists(null, $this->aChoices))
-        {
-            $this->aChoices = array(null => static::DEFAULT_NULL_CHOICE_LABEL) + $this->aChoices;
-        }
+		if (!array_key_exists(null, $this->aChoices))
+		{
+			$this->aChoices = array(null => static::DEFAULT_NULL_CHOICE_LABEL) + $this->aChoices;
+		}
 
-        return $this;
-    }
+		return $this;
+	}
 
-    /**
-     * Sets the choices for the fields
-     * Overloads the methods for the super class in order to put a dummy choice first if necessary.
-     * 
-     * @param array $aChoices
-     * @return \Combodo\iTop\Form\Field\SelectField
-     */
-    public function SetChoices($aChoices)
-    {
-        if ($this->bStartsWithNullChoice && !array_key_exists(null, $aChoices))
-        {
-            $aChoices = array(null => static::DEFAULT_NULL_CHOICE_LABEL) + $aChoices;
-        }
+	/**
+	 * Sets the choices for the fields
+	 * Overloads the methods for the super class in order to put a dummy choice first if necessary.
+	 *
+	 * @param array $aChoices
+	 * @return \Combodo\iTop\Form\Field\SelectField
+	 */
+	public function SetChoices($aChoices)
+	{
+		if ($this->bStartsWithNullChoice && !array_key_exists(null, $aChoices))
+		{
+			$aChoices = array(null => static::DEFAULT_NULL_CHOICE_LABEL) + $aChoices;
+		}
 
-        parent::SetChoices($aChoices);
-        return $this;
-    }
+		parent::SetChoices($aChoices);
+		return $this;
+	}
 
 }

+ 31 - 2
sources/form/field/subformfield.class.inc.php

@@ -23,7 +23,7 @@ use \Closure;
 use \Combodo\iTop\Form\Form;
 
 /**
- * Description of StringField
+ * Description of SubFormField
  *
  * @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
  */
@@ -31,12 +31,23 @@ class SubFormField extends Field
 {
 	protected $oForm;
 
+	/**
+	 * Default constructor
+	 *
+	 * @param string $sId
+	 * @param string $sParentFormId
+	 * @param Closure $onFinalizeCallback
+	 */
 	public function __construct($sId, $sParentFormId, Closure $onFinalizeCallback = null)
 	{
 		$this->oForm = new Form($sParentFormId.'-subform_'.$sId);
 		parent::__construct($sId, $onFinalizeCallback);
 	}
 
+	/**
+	 *
+	 * @return \Combodo\iTop\Form\Form
+	 */
 	public function GetForm()
 	{
 		return $this->oForm;
@@ -53,11 +64,19 @@ class SubFormField extends Field
 		$this->oForm->Validate();
 	}
 
+	/**
+	 *
+	 * @return boolean
+	 */
 	public function GetValid()
 	{
 		return $this->oForm->GetValid();
 	}
 
+	/**
+	 *
+	 * @return array
+	 */
 	public function GetErrorMessages()
 	{
 		$aRet = array();
@@ -68,13 +87,23 @@ class SubFormField extends Field
 		return $aRet;
 	}
 
+	/**
+	 *
+	 * @return array
+	 */
 	public function GetCurrentValue()
 	{
 		return $this->oForm->GetCurrentValues();
 	}
 
+	/**
+	 *
+	 * @param array $value
+	 * @return \Combodo\iTop\Form\Field\SubFormField
+	 */
 	public function SetCurrentValue($value)
 	{
-		return $this->oForm->SetCurrentValues($value);
+		$this->oForm->SetCurrentValues($value);
+		return $this;
 	}
 }

+ 1 - 1
sources/form/field/textareafield.class.inc.php

@@ -28,5 +28,5 @@ use \Combodo\iTop\Form\Field\TextField;
  */
 class TextAreaField extends TextField
 {
-    
+
 }

+ 20 - 20
sources/form/field/textfield.class.inc.php

@@ -29,26 +29,26 @@ use \Combodo\iTop\Form\Field\Field;
 abstract class TextField extends Field
 {
 
-    /**
-     * Checks the validators to see if the field's current value is valid.
-     * Then sets $bValid and $aErrorMessages.
-     * 
-     * @return boolean
-     */
-    public function Validate()
-    {
-        $this->SetValid(true);
-        $this->EmptyErrorMessages();
-        foreach ($this->GetValidators() as $oValidator)
-        {
-            if (!preg_match($oValidator->GetRegExp(true), $this->GetCurrentValue()))
-            {
-                $this->SetValid(false);
-                $this->AddErrorMessage($oValidator->GetErrorMessage());
-            }
-        }
+	/**
+	 * Checks the validators to see if the field's current value is valid.
+	 * Then sets $bValid and $aErrorMessages.
+	 *
+	 * @return boolean
+	 */
+	public function Validate()
+	{
+		$this->SetValid(true);
+		$this->EmptyErrorMessages();
+		foreach ($this->GetValidators() as $oValidator)
+		{
+			if (!preg_match($oValidator->GetRegExp(true), $this->GetCurrentValue()))
+			{
+				$this->SetValid(false);
+				$this->AddErrorMessage($oValidator->GetErrorMessage());
+			}
+		}
 
-        return $this->GetValid();
-    }
+		return $this->GetValid();
+	}
 
 }

+ 338 - 263
sources/form/form.class.inc.php

@@ -30,268 +30,343 @@ use \Combodo\iTop\Form\Field\Field;
  */
 class Form
 {
-    protected $sId;
-    protected $aFields;
-    protected $aDependencies;
-    protected $bValid;
-    protected $aErrorMessages;
-
-    public function __construct($sId)
-    {
-        $this->sId = $sId;
-        $this->aFields = array();
-        $this->aDependencies = array();
-        $this->bValid = true;
-        $this->aErrorMessages = array();
-    }
-
-    public function GetId()
-    {
-        return $this->sId;
-    }
-
-    public function GetFields()
-    {
-        return $this->aFields;
-    }
-
-    public function GetDependencies()
-    {
-        return $this->aDependencies;
-    }
-
-    public function GetCurrentValues()
-    {
-        $aValues = array();
-        foreach ($this->aFields as $sId => $oField)
-        {
-            $aValues[$sId] = $oField->GetCurrentValue();
-        }
-        return $aValues;
-    }
-
-    public function SetCurrentValues($aValues)
-    {
-        foreach ($aValues as $sId => $value)
-        {
-            $oField = $this->GetField($sId);
-            $oField->SetCurrentValue($value);
-        }
-    }
-
-    /**
-     * Returns the current validation state of the form (true|false).
-     * It DOESN'T make the validation, see Validate() instead.
-     *
-     * @return boolean
-     */
-    public function GetValid()
-    {
-        return $this->bValid;
-    }
-
-    /**
-     * Note : Function is protected as bValid should not be set from outside
-     *
-     * @param boolean $bValid
-     * @return \Combodo\iTop\Form\Form
-     */
-    protected function SetValid($bValid)
-    {
-        $this->bValid = $bValid;
-        return $this;
-    }
-
-    public function GetErrorMessages()
-    {
-        return $this->aErrorMessages;
-    }
-
-    /**
-     * Note : Function is protected as aErrorMessages should not be set from outside
-     *
-     * @param array $aErrorMessages
-     * @param string $sFieldId
-     * @return \Combodo\iTop\Form\Form
-     */
-    protected function SetErrorMessages($aErrorMessages, $sFieldId = null)
-    {
-        if ($sFieldId === null)
-        {
-            $this->aErrorMessages = $aErrorMessages;
-        }
-        else
-        {
-            $this->aErrorMessages[$sFieldId] = $aErrorMessages;
-        }
-        return $this;
-    }
-
-    /**
-     * If $sFieldId is not set, the $sErrorMessage will be added to the general form messages
-     *
-     * Note : Function is protected as aErrorMessages should not be add from outside
-     *
-     * @param string $sErrorMessage
-     * @param string $sFieldId
-     * @return \Combodo\iTop\Form\Form
-     */
-    protected function AddErrorMessage($sErrorMessage, $sFieldId = '_main')
-    {
-        if (!isset($this->aErrorMessages[$sFieldId]))
-        {
-            $this->aErrorMessages[$sFieldId] = array();
-        }
-        $this->aErrorMessages[$sFieldId][] = $sErrorMessage;
-        return $this;
-    }
-
-    /**
-     * Note : Function is protected as aErrorMessages should not be set from outside
-     *
-     * @return \Combodo\iTop\Form\Form
-     */
-    protected function EmptyErrorMessages()
-    {
-        $this->aErrorMessages = array();
-        return $this;
-    }
-
-    public function GetField($sId)
-    {
-        if (!array_key_exists($sId, $this->aFields))
-        {
-            throw new Exception('Field with ID "' . $sId . '" was not found in the Form.');
-        }
-        return $this->aFields[$sId];
-    }
-
-    public function HasField($sId)
-    {
-        return array_key_exists($sId, $this->aFields);
-    }
-
-    public function AddField(Field $oField, $aDependsOnIds = array())
-    {
-        $oField->SetFormPath($this->sId);
-        $this->aFields[$oField->GetId()] = $oField;
-        return $this;
-    }
-
-    public function RemoveField($sId)
-    {
-        if (array_key_exists($sId, $this->aFields))
-        {
-            unset($this->aFields[$sId]);
-        }
-        return $this;
-    }
-
-    /**
-     * Returns a array (list) of the fields ordered by their dependencies.
-     * 
-     * @return array
-     */
-    public function GetOrderedFields()
-    {
-        // TODO : Do this so it flatten the array
-        return $this->aFields;
-    }
-
-    /**
-     * Returns an array of field ids the $sFieldId depends on.
-     *
-     * @param string $sFieldId
-     * @return array
-     * @throws Exception
-     */
-    public function GetFieldDependencies($sFieldId)
-    {
-        if (!array_key_exists($sFieldId, $this->aDependencies))
-        {
-            throw new Exception('Field with ID "' . $sFieldId . '" had no dependancies declared in the Form.');
-        }
-        return $this->aDependencies[$sFieldId];
-    }
-
-    public function AddFieldDependencies($sFieldId, array $aDependsOnIds)
-    {
-        foreach ($aDependsOnIds as $sDependsOnId)
-        {
-            $this->AddFieldDependency($sFieldId, $sDependsOnId);
-        }
-        return $this;
-    }
-
-    public function AddFieldDependency($sFieldId, $sDependsOnId)
-    {
-        if (!array_key_exists($sFieldId, $this->aDependencies))
-        {
-            $this->aDependencies[$sFieldId] = array();
-        }
-        $this->aDependencies[$sFieldId][] = $sDependsOnId;
-        return $this;
-    }
-
-    /**
-     * Returns a hash array of the fields impacts on other fields. Key being the field that impacts the fields stored in the value as a regular array
-     * (It kind of reversed the dependencies array)
-     *
-     * eg :
-     * - 'service' => array('subservice', 'template')
-     * - 'subservice' => array()
-     * - ...
-     *
-     * @return array
-     */
-    public function GetFieldsImpacts()
-    {
-        $aRes = array();
-
-        foreach ($this->aDependencies as $sImpactedFieldId => $aDependentFieldsIds)
-        {
-            foreach ($aDependentFieldsIds as $sDependentFieldId)
-            {
-                if (!array_key_exists($sDependentFieldId, $aRes))
-                {
-                    $aRes[$sDependentFieldId] = array();
-                }
-                $aRes[$sDependentFieldId][] = $sImpactedFieldId;
-            }
-        }
-
-        return $aRes;
-    }
-
-    public function Finalize()
-    {
-        //TODO : Call GetOrderedFields
-        // Must call OnFinalize on each fields, regarding the dependencies order
-        // On a SubFormField, will call its own Finalize
-        foreach ($this->aFields as $sId => $oField)
-        {
-            $oField->OnFinalize();
-        }
-    }
-
-    public function Validate()
-    {
-        $this->SetValid(true);
-        $this->EmptyErrorMessages();
-        
-        foreach ($this->aFields as $oField)
-        {
-            if (!$oField->Validate())
-            {
-                $this->SetValid(false);
-                foreach ($oField->GetErrorMessages() as $sErrorMessage)
-                {
-                    $this->AddErrorMessage(Dict::S($sErrorMessage), $oField->Getid());
-                }
-            }
-        }
-        
-        return $this->GetValid();
-    }
+	protected $sId;
+	protected $aFields;
+	protected $aDependencies;
+	protected $bValid;
+	protected $aErrorMessages;
+
+	/**
+	 * Default constructor
+	 *
+	 * @param string $sId
+	 */
+	public function __construct($sId)
+	{
+		$this->sId = $sId;
+		$this->aFields = array();
+		$this->aDependencies = array();
+		$this->bValid = true;
+		$this->aErrorMessages = array();
+	}
+
+	/**
+	 *
+	 * @return string
+	 */
+	public function GetId()
+	{
+		return $this->sId;
+	}
+
+	/**
+	 *
+	 * @return array
+	 */
+	public function GetFields()
+	{
+		return $this->aFields;
+	}
+
+	/**
+	 *
+	 * @return array
+	 */
+	public function GetDependencies()
+	{
+		return $this->aDependencies;
+	}
+
+	/**
+	 * Returns a hash array of "Field id" => "Field value"
+	 *
+	 * @return array
+	 */
+	public function GetCurrentValues()
+	{
+		$aValues = array();
+		foreach ($this->aFields as $sId => $oField)
+		{
+			$aValues[$sId] = $oField->GetCurrentValue();
+		}
+		return $aValues;
+	}
+
+	/**
+	 *
+	 * @param array $aValues Must be a hash array of "Field id" => "Field value"
+	 * @return \Combodo\iTop\Form\Form
+	 */
+	public function SetCurrentValues($aValues)
+	{
+		foreach ($aValues as $sId => $value)
+		{
+			$oField = $this->GetField($sId);
+			$oField->SetCurrentValue($value);
+		}
+
+		return $this;
+	}
+
+	/**
+	 * Returns the current validation state of the form (true|false).
+	 * It DOESN'T make the validation, see Validate() instead.
+	 *
+	 * @return boolean
+	 */
+	public function GetValid()
+	{
+		return $this->bValid;
+	}
+
+	/**
+	 * Note : Function is protected as bValid should not be set from outside
+	 *
+	 * @param boolean $bValid
+	 * @return \Combodo\iTop\Form\Form
+	 */
+	protected function SetValid($bValid)
+	{
+		$this->bValid = $bValid;
+		return $this;
+	}
+
+	/**
+	 *
+	 * @return array
+	 */
+	public function GetErrorMessages()
+	{
+		return $this->aErrorMessages;
+	}
+
+	/**
+	 * Note : Function is protected as aErrorMessages should not be set from outside
+	 *
+	 * @param array $aErrorMessages
+	 * @param string $sFieldId
+	 * @return \Combodo\iTop\Form\Form
+	 */
+	protected function SetErrorMessages($aErrorMessages, $sFieldId = null)
+	{
+		if ($sFieldId === null)
+		{
+			$this->aErrorMessages = $aErrorMessages;
+		}
+		else
+		{
+			$this->aErrorMessages[$sFieldId] = $aErrorMessages;
+		}
+		return $this;
+	}
+
+	/**
+	 * If $sFieldId is not set, the $sErrorMessage will be added to the general form messages
+	 *
+	 * Note : Function is protected as aErrorMessages should not be add from outside
+	 *
+	 * @param string $sErrorMessage
+	 * @param string $sFieldId
+	 * @return \Combodo\iTop\Form\Form
+	 */
+	protected function AddErrorMessage($sErrorMessage, $sFieldId = '_main')
+	{
+		if (!isset($this->aErrorMessages[$sFieldId]))
+		{
+			$this->aErrorMessages[$sFieldId] = array();
+		}
+		$this->aErrorMessages[$sFieldId][] = $sErrorMessage;
+		return $this;
+	}
+
+	/**
+	 * Note : Function is protected as aErrorMessages should not be set from outside
+	 *
+	 * @return \Combodo\iTop\Form\Form
+	 */
+	protected function EmptyErrorMessages()
+	{
+		$this->aErrorMessages = array();
+		return $this;
+	}
+
+	/**
+	 *
+	 * @param string $sId
+	 * @return \Combodo\iTop\Form\Field\Field
+	 * @throws Exception
+	 */
+	public function GetField($sId)
+	{
+		if (!array_key_exists($sId, $this->aFields))
+		{
+			throw new Exception('Field with ID "' . $sId . '" was not found in the Form.');
+		}
+		return $this->aFields[$sId];
+	}
+
+	/**
+	 *
+	 * @param string $sId
+	 * @return boolean
+	 */
+	public function HasField($sId)
+	{
+		return array_key_exists($sId, $this->aFields);
+	}
+
+	/**
+	 *
+	 * @param \Combodo\iTop\Form\Field\Field $oField
+	 * @param array $aDependsOnIds
+	 * @return \Combodo\iTop\Form\Form
+	 */
+	public function AddField(Field $oField, $aDependsOnIds = array())
+	{
+		$oField->SetFormPath($this->sId);
+		$this->aFields[$oField->GetId()] = $oField;
+		return $this;
+	}
+
+	/**
+	 *
+	 * @param string $sId
+	 * @return \Combodo\iTop\Form\Form
+	 */
+	public function RemoveField($sId)
+	{
+		if (array_key_exists($sId, $this->aFields))
+		{
+			unset($this->aFields[$sId]);
+		}
+		return $this;
+	}
+
+	/**
+	 * Returns a array (list) of the fields ordered by their dependencies.
+	 *
+	 * @return array
+	 */
+	public function GetOrderedFields()
+	{
+		// TODO : Do this so it flatten the array
+		return $this->aFields;
+	}
+
+	/**
+	 * Returns an array of field ids the $sFieldId depends on.
+	 *
+	 * @param string $sFieldId
+	 * @return array
+	 * @throws Exception
+	 */
+	public function GetFieldDependencies($sFieldId)
+	{
+		if (!array_key_exists($sFieldId, $this->aDependencies))
+		{
+			throw new Exception('Field with ID "' . $sFieldId . '" had no dependancies declared in the Form.');
+		}
+		return $this->aDependencies[$sFieldId];
+	}
+
+	/**
+	 *
+	 * @param string $sFieldId
+	 * @param array $aDependsOnIds
+	 * @return \Combodo\iTop\Form\Form
+	 */
+	public function AddFieldDependencies($sFieldId, array $aDependsOnIds)
+	{
+		foreach ($aDependsOnIds as $sDependsOnId)
+		{
+			$this->AddFieldDependency($sFieldId, $sDependsOnId);
+		}
+		return $this;
+	}
+
+	/**
+	 *
+	 * @param string $sFieldId
+	 * @param string $sDependsOnId
+	 * @return \Combodo\iTop\Form\Form
+	 */
+	public function AddFieldDependency($sFieldId, $sDependsOnId)
+	{
+		if (!array_key_exists($sFieldId, $this->aDependencies))
+		{
+			$this->aDependencies[$sFieldId] = array();
+		}
+		$this->aDependencies[$sFieldId][] = $sDependsOnId;
+		return $this;
+	}
+
+	/**
+	 * Returns a hash array of the fields impacts on other fields. Key being the field that impacts the fields stored in the value as a regular array
+	 * (It kind of reversed the dependencies array)
+	 *
+	 * eg :
+	 * - 'service' => array('subservice', 'template')
+	 * - 'subservice' => array()
+	 * - ...
+	 *
+	 * @return array
+	 */
+	public function GetFieldsImpacts()
+	{
+		$aRes = array();
+
+		foreach ($this->aDependencies as $sImpactedFieldId => $aDependentFieldsIds)
+		{
+			foreach ($aDependentFieldsIds as $sDependentFieldId)
+			{
+				if (!array_key_exists($sDependentFieldId, $aRes))
+				{
+					$aRes[$sDependentFieldId] = array();
+				}
+				$aRes[$sDependentFieldId][] = $sImpactedFieldId;
+			}
+		}
+
+		return $aRes;
+	}
+
+	/**
+	 *
+	 */
+	public function Finalize()
+	{
+		//TODO : Call GetOrderedFields
+		// Must call OnFinalize on each fields, regarding the dependencies order
+		// On a SubFormField, will call its own Finalize
+		foreach ($this->aFields as $sId => $oField)
+		{
+			$oField->OnFinalize();
+		}
+	}
+
+	/**
+	 * Validate the form and return if it's valid or not
+	 * 
+	 * @return boolean
+	 */
+	public function Validate()
+	{
+		$this->SetValid(true);
+		$this->EmptyErrorMessages();
+
+		foreach ($this->aFields as $oField)
+		{
+			if (!$oField->Validate())
+			{
+				$this->SetValid(false);
+				foreach ($oField->GetErrorMessages() as $sErrorMessage)
+				{
+					$this->AddErrorMessage(Dict::S($sErrorMessage), $oField->Getid());
+				}
+			}
+		}
+
+		return $this->GetValid();
+	}
 
 }

+ 100 - 61
sources/form/formmanager.class.inc.php

@@ -29,72 +29,111 @@ use \Combodo\iTop\Renderer\FormRenderer;
  */
 abstract class FormManager
 {
-    protected $oForm;
-    protected $oRenderer;
+	protected $oForm;
+	protected $oRenderer;
 
-    static function FromJSON($sJson)
-    {
-        // Overload in child class when needed
-        $aJson = json_decode($sJson, true);
+	/**
+	 * Creates an instance of \Combodo\iTop\Form\FormManager from JSON data that must contain at least :
+	 * - formrenderer_class : The class of the FormRenderer to use in the FormManager
+	 * - formrenderer_endpoint : The endpoint of the renderer
+	 *
+	 * @param string $sJson
+	 * @return \Combodo\iTop\Form\FormManager
+	 */
+	static function FromJSON($sJson)
+	{
+		// Overload in child class when needed
+		$aJson = json_decode($sJson, true);
+
+		$oFormManager = new static();
+
+		$sFormRendererClass = $aJson['formrenderer_class'];
+		$oFormRenderer = new $sFormRendererClass();
+		$oFormRenderer->SetEndpoint($aJson['formrenderer_endpoint']);
+		$oFormManager->SetRenderer($oFormRenderer);
+
+		return $oFormManager;
+	}
+
+	public function __construct()
+	{
+		// Overload in child class when needed
+	}
 
-        $oFormManager = new static();
+	/**
+	 *
+	 * @return \Combodo\iTop\Form\Form
+	 */
+	public function GetForm()
+	{
+		return $this->oForm;
+	}
 
-        $sFormRendererClass = $aJson['formrenderer_class'];
-        $oFormRenderer = new $sFormRendererClass();
-        $oFormRenderer->SetEndpoint($aJson['formrenderer_endpoint']);
-        $oFormManager->SetRenderer($oFormRenderer);
+	/**
+	 *
+	 * @param \Combodo\iTop\Form\Form $oForm
+	 * @return \Combodo\iTop\Form\FormManager
+	 */
+	public function SetForm(Form $oForm)
+	{
+		$this->oForm = $oForm;
+		return $this;
+	}
 
-        return $oFormManager;
-    }
+	/**
+	 *
+	 * @return \Combodo\iTop\Renderer\FormRenderer
+	 */
+	public function GetRenderer()
+	{
+		return $this->oRenderer;
+	}
 
-    public function __construct()
-    {
-        // Overload in child class when needed
-    }
+	/**
+	 *
+	 * @param \Combodo\iTop\Renderer\FormRenderer $oRenderer
+	 * @return \Combodo\iTop\Form\FormManager
+	 */
+	public function SetRenderer(FormRenderer $oRenderer)
+	{
+		$this->oRenderer = $oRenderer;
+		return $this;
+	}
+
+	/**
+	 *
+	 * @return string
+	 */
+	public function GetClass()
+	{
+		return get_class($this);
+	}
 
 	/**
-     * @return Form
-     */
-    public function GetForm()
-    {
-        return $this->oForm;
-    }
-
-    /**
-     * @return FormRenderer
-     */
-    public function GetRenderer()
-    {
-        return $this->oRenderer;
-    }
-
-    public function SetRenderer(FormRenderer $oRenderer)
-    {
-        $this->oRenderer = $oRenderer;
-        return $this;
-    }
-
-    public function GetClass()
-    {
-        return get_class($this);
-    }
-
-    public function ToJSON()
-    {
-        // Overload in child class when needed
-        return array(
-            'id' => $this->oForm->GetId(),
-            'formmanager_class' => $this->GetClass(),
-            'formrenderer_class' => get_class($this->GetRenderer()),
-            'formrenderer_endpoint' => $this->GetRenderer()->GetEndpoint()
-        );
-    }
-
-    abstract public function Build();
-
-    abstract public function OnUpdate($aArgs = null);
-
-    abstract public function OnSubmit($aArgs = null);
-
-    abstract public function OnCancel($aArgs = null);
+	 * Creates a JSON string from the current object including :
+	 * - id : Id of the current Form
+	 * - formmanager_class
+	 * - formrenderer_class
+	 * - formrenderer_endpoint
+	 *
+	 * @return string
+	 */
+	public function ToJSON()
+	{
+		// Overload in child class when needed
+		return array(
+			'id' => $this->oForm->GetId(),
+			'formmanager_class' => $this->GetClass(),
+			'formrenderer_class' => get_class($this->GetRenderer()),
+			'formrenderer_endpoint' => $this->GetRenderer()->GetEndpoint()
+		);
+	}
+
+	abstract public function Build();
+
+	abstract public function OnUpdate($aArgs = null);
+
+	abstract public function OnSubmit($aArgs = null);
+
+	abstract public function OnCancel($aArgs = null);
 }

+ 3 - 3
sources/form/validator/integervalidator.class.inc.php

@@ -28,8 +28,8 @@ use \Combodo\iTop\Form\Validator\Validator;
  */
 class IntegerValidator extends Validator
 {
-    const VALIDATOR_NAME = 'integer';
-    const DEFAULT_REGEXP = '^[0-9]+$';
-    const DEFAULT_ERROR_MESSAGE = 'TOTR: MUST NOT BE AN INTEGER MESSAGE';
+	const VALIDATOR_NAME = 'integer';
+	const DEFAULT_REGEXP = '^[0-9]+$';
+	const DEFAULT_ERROR_MESSAGE = 'TOTR: MUST NOT BE AN INTEGER MESSAGE';
 
 }

+ 3 - 3
sources/form/validator/mandatoryvalidator.class.inc.php

@@ -30,8 +30,8 @@ use \Combodo\iTop\Form\Validator\Validator;
  */
 class MandatoryValidator extends Validator
 {
-    const VALIDATOR_NAME = 'mandatory';
-    const DEFAULT_REGEXP = '.*\S.*';
-    const DEFAULT_ERROR_MESSAGE = 'TOTR: MANDATORY MESSAGE';
+	const VALIDATOR_NAME = 'mandatory';
+	const DEFAULT_REGEXP = '.*\S.*';
+	const DEFAULT_ERROR_MESSAGE = 'TOTR: MANDATORY MESSAGE';
 
 }

+ 3 - 3
sources/form/validator/notemptyextkeyvalidator.class.inc.php

@@ -28,8 +28,8 @@ use \Combodo\iTop\Form\Validator\Validator;
  */
 class NotEmptyExtKeyValidator extends Validator
 {
-    const VALIDATOR_NAME = 'notemptyextkey';
-    const DEFAULT_REGEXP = '^[1-9]+$';
-    const DEFAULT_ERROR_MESSAGE = 'TOTR: MUST SELECT ONE';
+	const VALIDATOR_NAME = 'notemptyextkey';
+	const DEFAULT_REGEXP = '^[1-9]+$';
+	const DEFAULT_ERROR_MESSAGE = 'TOTR: MUST SELECT ONE';
 
 }

+ 3 - 3
sources/form/validator/notemptyvalidator.class.inc.php

@@ -28,8 +28,8 @@ use \Combodo\iTop\Form\Validator\Validator;
  */
 class NotEmptyValidator extends Validator
 {
-    const VALIDATOR_NAME = 'notempty';
-    const DEFAULT_REGEXP = '.*\S.*';
-    const DEFAULT_ERROR_MESSAGE = 'TOTR: MUST NOT BE EMPTY MESSAGE';
+	const VALIDATOR_NAME = 'notempty';
+	const DEFAULT_REGEXP = '.*\S.*';
+	const DEFAULT_ERROR_MESSAGE = 'TOTR: MUST NOT BE EMPTY MESSAGE';
 
 }

+ 68 - 68
sources/form/validator/validator.class.inc.php

@@ -26,83 +26,83 @@ namespace Combodo\iTop\Form\Validator;
  */
 class Validator
 {
-    const VALIDATOR_NAME = 'expression';
-    const DEFAULT_REGEXP = '';
-    const DEFAULT_ERROR_MESSAGE = 'TOTR: Core:Validator:Default';
+	const VALIDATOR_NAME = 'expression';
+	const DEFAULT_REGEXP = '';
+	const DEFAULT_ERROR_MESSAGE = 'TOTR: Core:Validator:Default';
 
-    protected $sRegExp;
-    protected $sErrorMessage;
+	protected $sRegExp;
+	protected $sErrorMessage;
 
-    public static function GetName()
-    {
-        return static::VALIDATOR_NAME;
-    }
+	public static function GetName()
+	{
+		return static::VALIDATOR_NAME;
+	}
 
-    /**
-     *
-     * @param Closure $callback (Used in the $oForm->AddField($sId, ..., function() use ($oManager, $oForm, '...') { ... } ); )
-     */
-    public function __construct($sRegExp = null, $sErrorMessage = null)
-    {
-        $this->sRegExp = ($sRegExp === null) ? static::DEFAULT_REGEXP : $sRegExp;
-        $this->sErrorMessage = ($sErrorMessage === null) ? static::DEFAULT_ERROR_MESSAGE : $sErrorMessage;
-        $this->ComputeConstraints();
-    }
+	/**
+	 *
+	 * @param Closure $callback (Used in the $oForm->AddField($sId, ..., function() use ($oManager, $oForm, '...') { ... } ); )
+	 */
+	public function __construct($sRegExp = null, $sErrorMessage = null)
+	{
+		$this->sRegExp = ($sRegExp === null) ? static::DEFAULT_REGEXP : $sRegExp;
+		$this->sErrorMessage = ($sErrorMessage === null) ? static::DEFAULT_ERROR_MESSAGE : $sErrorMessage;
+		$this->ComputeConstraints();
+	}
 
-    /**
-     * Returns the regular expression of the validator.
-     *
-     * @param boolean $bWithSlashes If true, surrounds $sRegExp with '/'. Used with preg_match & co
-     * @return string
-     */
-    public function GetRegExp($bWithSlashes = false)
-    {
-        return ($bWithSlashes) ? '/' . $this->sRegExp . '/' : $this->sRegExp;
-    }
+	/**
+	 * Returns the regular expression of the validator.
+	 *
+	 * @param boolean $bWithSlashes If true, surrounds $sRegExp with '/'. Used with preg_match & co
+	 * @return string
+	 */
+	public function GetRegExp($bWithSlashes = false)
+	{
+		return ($bWithSlashes) ? '/' . $this->sRegExp . '/' : $this->sRegExp;
+	}
 
-    public function GetErrorMessage()
-    {
-        return $this->sErrorMessage;
-    }
+	public function GetErrorMessage()
+	{
+		return $this->sErrorMessage;
+	}
 
-    public function SetRegExp($sRegExp)
-    {
-        $this->sRegExp = $sRegExp;
-        $this->ComputeConstraints();
-        return $this;
-    }
+	public function SetRegExp($sRegExp)
+	{
+		$this->sRegExp = $sRegExp;
+		$this->ComputeConstraints();
+		return $this;
+	}
 
-    public function SetErrorMessage($sErrorMessage)
-    {
-        $this->sErrorMessage = $sErrorMessage;
-        $this->ComputeConstraints();
-        return $this;
-    }
+	public function SetErrorMessage($sErrorMessage)
+	{
+		$this->sErrorMessage = $sErrorMessage;
+		$this->ComputeConstraints();
+		return $this;
+	}
 
-    /**
-     * Computes the regular expression and error message when changing constraints on the validator.
-     * Should be called in the validator's setters.
-     */
-    public function ComputeConstraints()
-    {
-        $this->ComputeRegularExpression();
-        $this->ComputeErrorMessage();
-    }
+	/**
+	 * Computes the regular expression and error message when changing constraints on the validator.
+	 * Should be called in the validator's setters.
+	 */
+	public function ComputeConstraints()
+	{
+		$this->ComputeRegularExpression();
+		$this->ComputeErrorMessage();
+	}
 
-    /**
-     * Computes the regular expression when changing constraints on the validator.
-     */
-    public function ComputeRegularExpression()
-    {
-        // Overload when necessary
-    }
+	/**
+	 * Computes the regular expression when changing constraints on the validator.
+	 */
+	public function ComputeRegularExpression()
+	{
+		// Overload when necessary
+	}
 
-    /**
-     * Computes the error message when changing constraints on the validator.
-     */
-    public function ComputeErrorMessage()
-    {
-        // Overload when necessary
-    }
+	/**
+	 * Computes the error message when changing constraints on the validator.
+	 */
+	public function ComputeErrorMessage()
+	{
+		// Overload when necessary
+	}
 
 }

+ 17 - 11
sources/renderer/bootstrap/bsformrenderer.class.inc.php

@@ -29,16 +29,22 @@ use \Combodo\iTop\Form\Form;
  */
 class BsFormRenderer extends FormRenderer
 {
-    const DEFAULT_RENDERER_NAMESPACE = 'Combodo\\iTop\\Renderer\\Bootstrap\\FieldRenderer\\';
+	const DEFAULT_RENDERER_NAMESPACE = 'Combodo\\iTop\\Renderer\\Bootstrap\\FieldRenderer\\';
+
+	/**
+	 * Default constructor
+	 * 
+	 * @param \Combodo\iTop\Form\Form $oForm
+	 */
+	public function __construct(Form $oForm = null)
+	{
+		parent::__construct($oForm);
+		$this->AddSupportedField('HiddenField', 'BsSimpleFieldRenderer');
+		$this->AddSupportedField('StringField', 'BsSimpleFieldRenderer');
+		$this->AddSupportedField('TextAreaField', 'BsSimpleFieldRenderer');
+		$this->AddSupportedField('SelectField', 'BsSimpleFieldRenderer');
+		$this->AddSupportedField('RadioField', 'BsSimpleFieldRenderer');
+		$this->AddSupportedField('CheckboxField', 'BsSimpleFieldRenderer');
+	}
 
-    public function __construct(Form $oForm = null)
-    {
-        parent::__construct($oForm);
-        $this->AddSupportedField('HiddenField', 'BsSimpleFieldRenderer');
-        $this->AddSupportedField('StringField', 'BsSimpleFieldRenderer');
-        $this->AddSupportedField('TextAreaField', 'BsSimpleFieldRenderer');
-        $this->AddSupportedField('SelectField', 'BsSimpleFieldRenderer');
-        $this->AddSupportedField('RadioField', 'BsSimpleFieldRenderer');
-        $this->AddSupportedField('CheckboxField', 'BsSimpleFieldRenderer');
-    }
 }

+ 200 - 192
sources/renderer/bootstrap/fieldrenderer/bssimplefieldrenderer.class.inc.php

@@ -31,201 +31,209 @@ use \Combodo\iTop\Renderer\RenderingOutput;
 class BsSimpleFieldRenderer extends FieldRenderer
 {
 
-    public function Render()
-    {
-        $oOutput = new RenderingOutput();
-        $sFieldClass = get_class($this->oField);
-        $sFieldId = 'field_' . spl_object_hash($this->oField);
-        $sFieldMandatoryClass = ($this->oField->GetMandatory()) ? 'form_mandatory' : '';
-
-        // TODO : Shouldn't we have a field type so we don't have to maintain FQN classname ?
-        // Rendering field in edition mode
-        if (!$this->oField->GetReadOnly())
-        {
-            switch ($sFieldClass)
-            {
-                case 'Combodo\\iTop\\Form\\Field\\StringField':
-                    $oOutput->AddHtml('<div class="form-group ' . $sFieldMandatoryClass . '">');
-                    if ($this->oField->GetLabel() !== '')
-                    {
-                        $oOutput->AddHtml('<label for="' . $sFieldId . '" class="control-label">' . $this->oField->GetLabel() . '</label>');
-                    }
-                    $oOutput->AddHtml('<div class="help-block"></div>');
-                    $oOutput->AddHtml('<input type="text" id="' . $sFieldId . '" name="' . $this->oField->GetId() . '" value="' . $this->oField->GetCurrentValue() . '" class="form-control" />');
-                    $oOutput->AddHtml('</div>');
-                    break;
-
-                case 'Combodo\\iTop\\Form\\Field\\TextAreaField':
-                    $oOutput->AddHtml('<div class="form-group ' . $sFieldMandatoryClass . '">');
-                    if ($this->oField->GetLabel() !== '')
-                    {
-                        $oOutput->AddHtml('<label for="' . $sFieldId . '" class="control-label">' . $this->oField->GetLabel() . '</label>');
-                    }
-                    $oOutput->AddHtml('<div class="help-block"></div>');
-                    $oOutput->AddHtml('<textarea id="' . $sFieldId . '" name="' . $this->oField->GetId() . '" class="form-control" rows="8">' . $this->oField->GetCurrentValue() . '</textarea>');
-                    $oOutput->AddHtml('</div>');
-                    break;
-
-                case 'Combodo\\iTop\\Form\\Field\\SelectField':
-                    $oOutput->AddHtml('<div class="form-group ' . $sFieldMandatoryClass . '">');
-                    if ($this->oField->GetLabel() !== '')
-                    {
-                        $oOutput->AddHtml('<label for="' . $sFieldId . '" class="control-label">' . $this->oField->GetLabel() . '</label>');
-                    }
-                    $oOutput->AddHtml('<div class="help-block"></div>');
-                    $oOutput->AddHtml('<select id="' . $sFieldId . '" name="' . $this->oField->GetId() . '" ' . ( ($this->oField->GetMultipleValuesEnabled()) ? 'multiple' : '' ) . ' class="form-control">');
-                    foreach ($this->oField->GetChoices() as $sChoice => $sLabel)
-                    {
-                        // Note : The test is a double equal on purpose as the type of the value received from the XHR is not always the same as the type of the allowed values. (eg : string vs int)
-                        $sSelectedAtt = ($this->oField->GetCurrentValue() == $sChoice) ? 'selected' : '';
-                        $oOutput->AddHtml('<option value="' . $sChoice . '" ' . $sSelectedAtt . ' >' . $sLabel . '</option>');
-                    }
-                    $oOutput->AddHtml('</select>');
-                    $oOutput->AddHtml('</div>');
-                    break;
-
-                case 'Combodo\\iTop\\Form\\Field\\RadioField':
-                case 'Combodo\\iTop\\Form\\Field\\CheckboxField':
-                    $sFieldType = ($sFieldClass === 'Combodo\\iTop\\Form\\Field\\RadioField') ? 'radio' : 'checkbox';
-
-                    $oOutput->AddHtml('<div class="form-group ' . $sFieldMandatoryClass . '" id="' . $sFieldId . '">');
-
-                    if ($this->oField->GetLabel() !== '')
-                    {
-                        $oOutput->AddHtml('<div><label class="control-label">' . $this->oField->GetLabel() . '</label></div>');
-                    }
-
-                    $oOutput->AddHtml('<div class="help-block"></div>');
-                    $oOutput->AddHtml('<div class="btn-group" data-toggle="buttons">');
-                    $i = 0;
-                    foreach ($this->oField->GetChoices() as $sChoice => $sLabel)
-                    {
-                        // Note : The test is a double equal on purpose as the type of the value received from the XHR is not always the same as the type of the allowed values. (eg : string vs int)
-                        $sCheckedAtt = ($this->oField->IsAmongValues($sChoice)) ? 'checked' : '';
-                        $sCheckedClass = ($this->oField->IsAmongValues($sChoice)) ? 'active' : '';
-                        $oOutput->AddHtml('<label class="btn btn-default ' . $sCheckedClass . '"><input type="' . $sFieldType . '" name="' . $this->oField->GetId() . '" id="' . $this->oField->GetId() . $i . '" value="' . $sChoice . '" ' . $sCheckedAtt . ' />' . $sLabel . '</label>');
-                        $i++;
-                    }
-                    $oOutput->AddHtml('</div>');
-
-                    $oOutput->AddHtml('</div>');
-                    break;
-
-                case 'Combodo\\iTop\\Form\\Field\\HiddenField':
-                    $oOutput->AddHtml('<input type="hidden" id="' . $sFieldId . '" name="' . $this->oField->GetId() . '" value="' . $this->oField->GetCurrentValue() . '"/>');
-                    break;
-            }
-        }
-        // ... and in read-only mode
-        else
-        {
-            // ... specific rendering for fields with mulltiple values
-            if (($this->oField instanceof Combodo\iTop\Form\Field\MultipleChoicesField) && ($this->oField->GetMultipleValuesEnabled()))
-            {
-                // TODO
-            }
-            // ... clasic rendering for fields with only one value
-            else
-            {
-                switch ($sFieldClass)
-                {
-                    case 'Combodo\\iTop\\Form\\Field\\StringField':
-                    case 'Combodo\\iTop\\Form\\Field\\TextAreaField':
-                        $oOutput->AddHtml('<div class="form-group">');
-                        if ($this->oField->GetLabel() !== '')
-                        {
-                            $oOutput->AddHtml('<label for="' . $sFieldId . '" class="control-label">' . $this->oField->GetLabel() . '</label>');
-                        }
-                        $oOutput->AddHtml('<div class="form-control-static">' . $this->oField->GetCurrentValue() . '</div>');
-                        $oOutput->AddHtml('<input type="hidden" id="' . $sFieldId . '" name="' . $this->oField->GetId() . '" value="' . $this->oField->GetCurrentValue() . '" class="form-control" />');
-                        $oOutput->AddHtml('</div>');
-                        break;
-
-                    case 'Combodo\\iTop\\Form\\Field\\RadioField':
-                    case 'Combodo\\iTop\\Form\\Field\\SelectField': // TODO : This should be check for external key, as we would display it differently
-                        $aFieldChoices = $this->oField->GetChoices();
-                        $sFieldValue = (isset($aFieldChoices[$this->oField->GetCurrentValue()])) ? $aFieldChoices[$this->oField->GetCurrentValue()] : Dict::S('UI:UndefinedObject');
-
-                        $oOutput->AddHtml('<div class="form-group">');
-                        if ($this->oField->GetLabel() !== '')
-                        {
-                            $oOutput->AddHtml('<label for="' . $sFieldId . '" class="control-label">' . $this->oField->GetLabel() . '</label>');
-                        }
-                        $oOutput->AddHtml('<div class="form-control-static">' . $sFieldValue . '</div>');
-                        $oOutput->AddHtml('<input type="hidden" id="' . $sFieldId . '" name="' . $this->oField->GetId() . '" value="' . $this->oField->GetCurrentValue() . '" class="form-control" />');
-                        $oOutput->AddHtml('</div>');
-                        break;
-                }
-            }
-        }
-
-        // JS FieldChange trigger (:input are not always at the same depth)
-        switch ($sFieldClass)
-        {
-            case 'Combodo\\iTop\\Form\\Field\\StringField':
-            case 'Combodo\\iTop\\Form\\Field\\TextAreaField':
-            case 'Combodo\\iTop\\Form\\Field\\SelectField':
-            case 'Combodo\\iTop\\Form\\Field\\HiddenField':
-                $oOutput->AddJs(
-<<<EOF
-                    $("#$sFieldId").off("change").on("change", function(){
-                        $(this).closest(".form_handler").trigger("field_change", {
-                            id: $(this).attr("id"),
-                            name: $(this).attr("name"),
-                            value: $(this).val()
-                        });
-                    });
+	/**
+	 * Returns a RenderingOutput for the FieldRenderer's Field
+	 *
+	 * @return \Combodo\iTop\Renderer\RenderingOutput
+	 */
+	public function Render()
+	{
+		$oOutput = new RenderingOutput();
+		$sFieldClass = get_class($this->oField);
+		$sFieldMandatoryClass = ($this->oField->GetMandatory()) ? 'form_mandatory' : '';
+
+		// TODO : Shouldn't we have a field type so we don't have to maintain FQN classname ?
+		// Rendering field in edition mode
+		if (!$this->oField->GetReadOnly())
+		{
+			switch ($sFieldClass)
+			{
+				case 'Combodo\\iTop\\Form\\Field\\StringField':
+					$oOutput->AddHtml('<div class="form-group ' . $sFieldMandatoryClass . '">');
+					if ($this->oField->GetLabel() !== '')
+					{
+						$oOutput->AddHtml('<label for="' . $this->oField->GetGlobalId() . '" class="control-label">' . $this->oField->GetLabel() . '</label>');
+					}
+					$oOutput->AddHtml('<div class="help-block"></div>');
+					$oOutput->AddHtml('<input type="text" id="' . $this->oField->GetGlobalId() . '" name="' . $this->oField->GetId() . '" value="' . $this->oField->GetCurrentValue() . '" class="form-control" />');
+					$oOutput->AddHtml('</div>');
+					break;
+
+				case 'Combodo\\iTop\\Form\\Field\\TextAreaField':
+					$oOutput->AddHtml('<div class="form-group ' . $sFieldMandatoryClass . '">');
+					if ($this->oField->GetLabel() !== '')
+					{
+						$oOutput->AddHtml('<label for="' . $this->oField->GetGlobalId() . '" class="control-label">' . $this->oField->GetLabel() . '</label>');
+					}
+					$oOutput->AddHtml('<div class="help-block"></div>');
+					$oOutput->AddHtml('<textarea id="' . $this->oField->GetGlobalId() . '" name="' . $this->oField->GetId() . '" class="form-control" rows="8">' . $this->oField->GetCurrentValue() . '</textarea>');
+					$oOutput->AddHtml('</div>');
+					break;
+
+				case 'Combodo\\iTop\\Form\\Field\\SelectField':
+					$oOutput->AddHtml('<div class="form-group ' . $sFieldMandatoryClass . '">');
+					if ($this->oField->GetLabel() !== '')
+					{
+						$oOutput->AddHtml('<label for="' . $this->oField->GetGlobalId() . '" class="control-label">' . $this->oField->GetLabel() . '</label>');
+					}
+					$oOutput->AddHtml('<div class="help-block"></div>');
+					$oOutput->AddHtml('<select id="' . $this->oField->GetGlobalId() . '" name="' . $this->oField->GetId() . '" ' . ( ($this->oField->GetMultipleValuesEnabled()) ? 'multiple' : '' ) . ' class="form-control">');
+					foreach ($this->oField->GetChoices() as $sChoice => $sLabel)
+					{
+						// Note : The test is a double equal on purpose as the type of the value received from the XHR is not always the same as the type of the allowed values. (eg : string vs int)
+						$sSelectedAtt = ($this->oField->GetCurrentValue() == $sChoice) ? 'selected' : '';
+						$oOutput->AddHtml('<option value="' . $sChoice . '" ' . $sSelectedAtt . ' >' . $sLabel . '</option>');
+					}
+					$oOutput->AddHtml('</select>');
+					$oOutput->AddHtml('</div>');
+					break;
+
+				case 'Combodo\\iTop\\Form\\Field\\RadioField':
+				case 'Combodo\\iTop\\Form\\Field\\CheckboxField':
+					$sFieldType = ($sFieldClass === 'Combodo\\iTop\\Form\\Field\\RadioField') ? 'radio' : 'checkbox';
+
+					$oOutput->AddHtml('<div class="form-group ' . $sFieldMandatoryClass . '" id="' . $this->oField->GetGlobalId() . '">');
+
+					if ($this->oField->GetLabel() !== '')
+					{
+						$oOutput->AddHtml('<div><label class="control-label">' . $this->oField->GetLabel() . '</label></div>');
+					}
+
+					$oOutput->AddHtml('<div class="help-block"></div>');
+					$oOutput->AddHtml('<div class="btn-group" data-toggle="buttons">');
+					$i = 0;
+					foreach ($this->oField->GetChoices() as $sChoice => $sLabel)
+					{
+						// Note : The test is a double equal on purpose as the type of the value received from the XHR is not always the same as the type of the allowed values. (eg : string vs int)
+						$sCheckedAtt = ($this->oField->IsAmongValues($sChoice)) ? 'checked' : '';
+						$sCheckedClass = ($this->oField->IsAmongValues($sChoice)) ? 'active' : '';
+						$oOutput->AddHtml('<label class="btn btn-default ' . $sCheckedClass . '"><input type="' . $sFieldType . '" name="' . $this->oField->GetId() . '" id="' . $this->oField->GetId() . $i . '" value="' . $sChoice . '" ' . $sCheckedAtt . ' />' . $sLabel . '</label>');
+						$i++;
+					}
+					$oOutput->AddHtml('</div>');
+
+					$oOutput->AddHtml('</div>');
+					break;
+
+				case 'Combodo\\iTop\\Form\\Field\\HiddenField':
+					$oOutput->AddHtml('<input type="hidden" id="' . $this->oField->GetGlobalId() . '" name="' . $this->oField->GetId() . '" value="' . $this->oField->GetCurrentValue() . '"/>');
+					break;
+			}
+		}
+		// ... and in read-only mode
+		else
+		{
+			// ... specific rendering for fields with mulltiple values
+			if (($this->oField instanceof Combodo\iTop\Form\Field\MultipleChoicesField) && ($this->oField->GetMultipleValuesEnabled()))
+			{
+				// TODO
+			}
+			// ... clasic rendering for fields with only one value
+			else
+			{
+				switch ($sFieldClass)
+				{
+					case 'Combodo\\iTop\\Form\\Field\\StringField':
+					case 'Combodo\\iTop\\Form\\Field\\TextAreaField':
+						$oOutput->AddHtml('<div class="form-group">');
+						if ($this->oField->GetLabel() !== '')
+						{
+							$oOutput->AddHtml('<label for="' . $this->oField->GetGlobalId() . '" class="control-label">' . $this->oField->GetLabel() . '</label>');
+						}
+						$oOutput->AddHtml('<div class="form-control-static">' . $this->oField->GetCurrentValue() . '</div>');
+						$oOutput->AddHtml('<input type="hidden" id="' . $this->oField->GetGlobalId() . '" name="' . $this->oField->GetId() . '" value="' . $this->oField->GetCurrentValue() . '" class="form-control" />');
+						$oOutput->AddHtml('</div>');
+						break;
+
+					case 'Combodo\\iTop\\Form\\Field\\RadioField':
+					case 'Combodo\\iTop\\Form\\Field\\SelectField': // TODO : This should be check for external key, as we would display it differently
+						$aFieldChoices = $this->oField->GetChoices();
+						$sFieldValue = (isset($aFieldChoices[$this->oField->GetCurrentValue()])) ? $aFieldChoices[$this->oField->GetCurrentValue()] : Dict::S('UI:UndefinedObject');
+
+						$oOutput->AddHtml('<div class="form-group">');
+						if ($this->oField->GetLabel() !== '')
+						{
+							$oOutput->AddHtml('<label for="' . $this->oField->GetGlobalId() . '" class="control-label">' . $this->oField->GetLabel() . '</label>');
+						}
+						$oOutput->AddHtml('<div class="form-control-static">' . $sFieldValue . '</div>');
+						$oOutput->AddHtml('<input type="hidden" id="' . $this->oField->GetGlobalId() . '" name="' . $this->oField->GetId() . '" value="' . $this->oField->GetCurrentValue() . '" class="form-control" />');
+						$oOutput->AddHtml('</div>');
+						break;
+				}
+			}
+		}
+
+		// JS FieldChange trigger (:input are not always at the same depth)
+		switch ($sFieldClass)
+		{
+			case 'Combodo\\iTop\\Form\\Field\\StringField':
+			case 'Combodo\\iTop\\Form\\Field\\TextAreaField':
+			case 'Combodo\\iTop\\Form\\Field\\SelectField':
+			case 'Combodo\\iTop\\Form\\Field\\HiddenField':
+				$oOutput->AddJs(
+					<<<EOF
+					$("#{$this->oField->GetGlobalId()}").off("change keyup").on("change keyup", function(){
+						var me = this;
+
+						$(this).closest(".field_set").trigger("field_change", {
+							id: $(me).attr("id"),
+							name: $(me).closest(".form_field").attr("data-field-id"),
+							value: $(me).val()
+						});
+					});
 EOF
-                );
-                break;
-
-            case 'Combodo\\iTop\\Form\\Field\\RadioField':
-            case 'Combodo\\iTop\\Form\\Field\\CheckboxField':
-                $oOutput->AddJs(
-<<<EOF
-                    $("#$sFieldId input").off("change").on("change", function(){
-                        $(this).closest(".form_handler").trigger("field_change", {
-                            id: $(this).closest("#$sFieldId").attr("id"),
-                            name: $(this).attr("name"),
-                            value: $(this).val()
-                        });
-                    });
+				);
+				break;
+
+			case 'Combodo\\iTop\\Form\\Field\\RadioField':
+			case 'Combodo\\iTop\\Form\\Field\\CheckboxField':
+				$oOutput->AddJs(
+					<<<EOF
+					$("#{$this->oField->GetGlobalId()} input").off("change").on("change", function(){
+						var me = this;
+
+						$(this).closest(".field_set").trigger("field_change", {
+							id: $(me).closest("#{$this->oField->GetGlobalId()}").attr("id"),
+							name: $(me).attr("name"),
+							value: $(me).val()
+						});
+					});
 EOF
-                );
-                break;
-        }
-
-        // JS Form field widget construct
-        $aValidators = array();
-        foreach ($this->oField->GetValidators() as $oValidator)
-        {
-            $aValidators[$oValidator::GetName()] = array(
-                'reg_exp' => $oValidator->GetRegExp(),
-                'message' => Dict::S($oValidator->GetErrorMessage())
-            );
-        }
-
-        $sFormFieldOptions = json_encode(array(
-            'validators' => $aValidators
-        ));
-
-        switch ($sFieldClass)
-        {
-            case 'Combodo\\iTop\\Form\\Field\\StringField':
-            case 'Combodo\\iTop\\Form\\Field\\TextAreaField':
-            case 'Combodo\\iTop\\Form\\Field\\SelectField':
-            case 'Combodo\\iTop\\Form\\Field\\HiddenField':
-            case 'Combodo\\iTop\\Form\\Field\\RadioField':
-            case 'Combodo\\iTop\\Form\\Field\\CheckboxField':
-                $oOutput->AddJs(
-                    <<<EOF
-                    $("[data-field-id='{$this->oField->GetId()}']").form_field($sFormFieldOptions);
+				);
+				break;
+		}
+
+		// JS Form field widget construct
+		$aValidators = array();
+		foreach ($this->oField->GetValidators() as $oValidator)
+		{
+			$aValidators[$oValidator::GetName()] = array(
+				'reg_exp' => $oValidator->GetRegExp(),
+				'message' => Dict::S($oValidator->GetErrorMessage())
+			);
+		}
+
+		$sFormFieldOptions = json_encode(array(
+			'validators' => $aValidators
+		));
+
+		switch ($sFieldClass)
+		{
+			case 'Combodo\\iTop\\Form\\Field\\StringField':
+			case 'Combodo\\iTop\\Form\\Field\\TextAreaField':
+			case 'Combodo\\iTop\\Form\\Field\\SelectField':
+			case 'Combodo\\iTop\\Form\\Field\\HiddenField':
+			case 'Combodo\\iTop\\Form\\Field\\RadioField':
+			case 'Combodo\\iTop\\Form\\Field\\CheckboxField':
+				$oOutput->AddJs(
+					<<<EOF
+					$("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").portal_form_field($sFormFieldOptions);
 EOF
-                );
-                break;
-        }
+				);
+				break;
+		}
 
-        return $oOutput;
-    }
+		return $oOutput;
+	}
 
 }

+ 37 - 29
sources/renderer/fieldrenderer.class.inc.php

@@ -28,33 +28,41 @@ use \Combodo\iTop\Form\Field\Field;
  */
 abstract class FieldRenderer
 {
-    protected $oField;
-    protected $sEndpoint;
-
-    /**
-     * Default constructor
-     * 
-     * @param \Combodo\iTop\Form\Field\Field $oField
-     */
-    public function __construct(Field $oField)
-    {
-        $this->oField = $oField;
-    }
-
-    public function GetEndpoint()
-    {
-        return $this->sEndpoint;
-    }
-
-    public function SetEndpoint($sEndpoint)
-    {
-        $this->sEndpoint = $sEndpoint;
-    }
-
-    /**
-     * Renders a Field as a RenderingOutput
-     *
-     * @return \Combodo\iTop\Renderer\RenderingOutput
-     */
-    abstract public function Render();
+	protected $oField;
+	protected $sEndpoint;
+
+	/**
+	 * Default constructor
+	 *
+	 * @param \Combodo\iTop\Form\Field\Field $oField
+	 */
+	public function __construct(Field $oField)
+	{
+		$this->oField = $oField;
+	}
+
+	/**
+	 *
+	 * @return string
+	 */
+	public function GetEndpoint()
+	{
+		return $this->sEndpoint;
+	}
+
+	/**
+	 *
+	 * @param string $sEndpoint
+	 */
+	public function SetEndpoint($sEndpoint)
+	{
+		$this->sEndpoint = $sEndpoint;
+	}
+
+	/**
+	 * Renders a Field as a RenderingOutput
+	 *
+	 * @return \Combodo\iTop\Renderer\RenderingOutput
+	 */
+	abstract public function Render();
 }

+ 242 - 200
sources/renderer/formrenderer.class.inc.php

@@ -22,6 +22,7 @@ namespace Combodo\iTop\Renderer;
 use \Exception;
 use \Dict;
 use \Combodo\iTop\Form\Form;
+use \Combodo\iTop\Form\Field\Field;
 
 /**
  * Description of FormRenderer
@@ -30,231 +31,272 @@ use \Combodo\iTop\Form\Form;
  */
 abstract class FormRenderer
 {
-    const ENUM_RENDER_MODE_EXPLODED = 'exploded';
-    const ENUM_RENDER_MODE_JOINED = 'joined';
-    const DEFAULT_RENDERER_NAMESPACE = '';
+	const ENUM_RENDER_MODE_EXPLODED = 'exploded';
+	const ENUM_RENDER_MODE_JOINED = 'joined';
+	const DEFAULT_RENDERER_NAMESPACE = '';
 
-    protected $oForm;
-    protected $sEndpoint;
-    protected $aSupportedFields;
-    protected $sBaseLayout;
-    protected $aOutputs;
+	protected $oForm;
+	protected $sEndpoint;
+	protected $aSupportedFields;
+	protected $sBaseLayout;
+	protected $aOutputs;
 
-    /**
-     * Default constructor
-     * 
-     * @param \Combodo\iTop\Form\Form $oForm
-     */
-    public function __construct(Form $oForm = null)
-    {
-        if ($oForm !== null)
-        {
-            $this->oForm = $oForm;
-        }
-        $this->sBaseLayout = '';
-        $this->InitOutputs();
-    }
+	/**
+	 * Default constructor
+	 *
+	 * @param \Combodo\iTop\Form\Form $oForm
+	 */
+	public function __construct(Form $oForm = null)
+	{
+		if ($oForm !== null)
+		{
+			$this->oForm = $oForm;
+		}
+		$this->sBaseLayout = '';
+		$this->InitOutputs();
+	}
 
-    public function GetForm()
-    {
-        return $this->oForm;
-    }
+	/**
+	 *
+	 * @return \Combodo\iTop\Form\Form
+	 */
+	public function GetForm()
+	{
+		return $this->oForm;
+	}
 
-    public function SetForm($oForm)
-    {
-        $this->oForm = $oForm;
-        return $this;
-    }
+	/**
+	 *
+	 * @param \Combodo\iTop\Form\Form $oForm
+	 * @return \Combodo\iTop\Renderer\FormRenderer
+	 */
+	public function SetForm(Form $oForm)
+	{
+		$this->oForm = $oForm;
+		return $this;
+	}
 
-    public function GetEndpoint()
-    {
-        return $this->sEndpoint;
-    }
+	/**
+	 *
+	 * @return string
+	 */
+	public function GetEndpoint()
+	{
+		return $this->sEndpoint;
+	}
 
-    public function SetEndpoint($sEndpoint)
-    {
-        $this->sEndpoint = $sEndpoint;
-        return $this;
-    }
+	/**
+	 *
+	 * @param string $sEndpoint
+	 * @return \Combodo\iTop\Renderer\FormRenderer
+	 */
+	public function SetEndpoint($sEndpoint)
+	{
+		$this->sEndpoint = $sEndpoint;
+		return $this;
+	}
 
-    public function GetBaseLayout()
-    {
-        return $this->sBaseLayout;
-    }
+	/**
+	 *
+	 * @return string
+	 */
+	public function GetBaseLayout()
+	{
+		return $this->sBaseLayout;
+	}
 
-    public function SetBaseLayout($sBaseLayout)
-    {
-        $this->sBaseLayout = $sBaseLayout;
-        return $this;
-    }
+	/**
+	 *
+	 * @param string $sBaseLayout
+	 * @return \Combodo\iTop\Renderer\FormRenderer
+	 */
+	public function SetBaseLayout($sBaseLayout)
+	{
+		$this->sBaseLayout = $sBaseLayout;
+		return $this;
+	}
 
-    public function GetFieldRendererClass($oField)
-    {
-        if (array_key_exists(get_class($oField), $this->aSupportedFields))
-        {
-            return $this->aSupportedFields[get_class($oField)];
-        }
-        else
-        {
-            throw new Exception('Field type not supported by the renderer: '.get_class($oField));
-        }
-    }
+	/**
+	 *
+	 * @param \Combodo\iTop\Form\Field\Field $oField
+	 * @return string
+	 * @throws Exception
+	 */
+	public function GetFieldRendererClass(Field $oField)
+	{
+		if (array_key_exists(get_class($oField), $this->aSupportedFields))
+		{
+			return $this->aSupportedFields[get_class($oField)];
+		}
+		else
+		{
+			throw new Exception('Field type not supported by the renderer: ' . get_class($oField));
+		}
+	}
 
-    /**
-     * Returns the field identified by the id $sId in $this->oForm.
-     *
-     * @param string $sId
-     * @return \Combodo\iTop\Renderer\FieldRenderer
-     */
-    public function GetFieldRendererClassFromId($sId)
-    {
-        return $this->GetFieldRendererClass($this->oForm->GetField($sId));
-    }
+	/**
+	 * Returns the field identified by the id $sId in $this->oForm.
+	 *
+	 * @param string $sId
+	 * @return \Combodo\iTop\Renderer\FieldRenderer
+	 */
+	public function GetFieldRendererClassFromId($sId)
+	{
+		return $this->GetFieldRendererClass($this->oForm->GetField($sId));
+	}
 
-    /**
-     *
-     * @return array
-     */
-    public function GetOutputs()
-    {
-        return $this->aOutputs;
-    }
+	/**
+	 *
+	 * @return array
+	 */
+	public function GetOutputs()
+	{
+		return $this->aOutputs;
+	}
 
+	/**
+	 * Registers a Renderer class for the specified Field class.
+	 *
+	 * If the Field class is not fully qualified, the default "Combodo\iTop\Form\Field" will be prepend.
+	 * If the Field class already had a registered Renderer, it is replaced.
+	 *
+	 * @param string $sFieldClass
+	 * @param string $sRendererClass
+	 */
+	public function AddSupportedField($sFieldClass, $sRendererClass)
+	{
+		$sFieldClass = (strpos($sFieldClass, '\\') !== false) ? $sFieldClass : 'Combodo\\iTop\\Form\\Field\\' . $sFieldClass;
+		$sRendererClass = (strpos($sRendererClass, '\\') !== false) ? $sRendererClass : static::DEFAULT_RENDERER_NAMESPACE . $sRendererClass;
 
-    /**
-     * Registers a Renderer class for the specified Field class.
-     * 
-     * If the Field class is not fully qualified, the default "Combodo\iTop\Form\Field" will be prepend.
-     * If the Field class already had a registered Renderer, it is replaced.
-     *
-     * @param string $sFieldClass
-     * @param string $sRendererClass
-     */
-    public function AddSupportedField($sFieldClass, $sRendererClass)
-    {
-        $sFieldClass = (strpos($sFieldClass, '\\') !== false) ? $sFieldClass : 'Combodo\\iTop\\Form\\Field\\' . $sFieldClass;
-        $sRendererClass = (strpos($sRendererClass, '\\') !== false) ? $sRendererClass : static::DEFAULT_RENDERER_NAMESPACE . $sRendererClass;
+		$this->aSupportedFields[$sFieldClass] = $sRendererClass;
 
-        $this->aSupportedFields[$sFieldClass] = $sRendererClass;
+		return $this;
+	}
 
-        return $this;
-    }
+	/**
+	 *
+	 * @return \Combodo\iTop\Renderer\FormRenderer
+	 */
+	public function InitOutputs()
+	{
+		$this->aOutputs = array();
+		return $this;
+	}
 
-    public function InitOutputs()
-    {
-        $this->aOutputs = array();
-        return $this;
-    }
+	/**
+	 *
+	 * @return array
+	 */
+	public function Render()
+	{
+		$this->InitOutputs();
 
-    public function Render()
-    {
-        $this->InitOutputs();
+		foreach ($this->oForm->GetFields() as $oField)
+		{
+			$this->aOutputs[$oField->GetId()] = $this->PrepareOutputForField($oField);
+		}
 
-        foreach ($this->oForm->GetFields() as $oField)
-        {
-            $this->aOutputs[$oField->GetId()] = $this->PrepareOutputForField($oField);
-        }
+		return $this->aOutputs;
+	}
 
-        return $this->aOutputs;
-    }
+	/**
+	 * Returns the output for the $oField. Output format depends on the $sMode.
+	 *
+	 * If $sMode = 'exploded', output is an has array with id / html / js_inline / js_files / css_inline / css_files / validators
+	 * Else if $sMode = 'joined', output is a string with everything in it
+	 *
+	 * @param \Combodo\iTop\Form\Field\Field $oField
+	 * @param string $sMode 'exploded'|'joined'
+	 * @return mixed
+	 */
+	protected function PrepareOutputForField($oField, $sMode = 'exploded')
+	{
+		$output = array(
+			'id' => $oField->GetId(),
+			'html' => '',
+			'js_inline' => '',
+			'css_inline' => '',
+			'js_files' => array(),
+			'css_files' => array()
+		);
 
-    /**
-     * Returns the output for the $oField. Output format depends on the $sMode.
-     *
-     * If $sMode = 'exploded', output is an has array with id / html / js_inline / js_files / css_inline / css_files / validators
-     * Else if $sMode = 'joined', output is a string with everything in it
-     *
-     * @param \Combodo\iTop\Form\Field\Field $oField
-     * @param string $sMode 'exploded'|'joined'
-     * @return mixed
-     */
-    protected function PrepareOutputForField($oField, $sMode = 'exploded')
-    {
-        $output = array(
-            'id' => $oField->GetId(),
-            'html' => '',
-            'js_inline' => '',
-            'css_inline' => '',
-            'js_files' => array(),
-            'css_files' => array()
-        );
+		$sFieldRendererClass = $this->GetFieldRendererClass($oField);
 
-        $sFieldRendererClass = $this->GetFieldRendererClass($oField);
+		$oFieldRenderer = new $sFieldRendererClass($oField);
+		$oFieldRenderer->SetEndpoint($this->GetEndpoint());
 
-        $oFieldRenderer = new $sFieldRendererClass($oField);
-        $oFieldRenderer->SetEndpoint($this->GetEndpoint());
+		$oRenderingOutput = $oFieldRenderer->Render();
 
-        $oRenderingOutput = $oFieldRenderer->Render();
+		// HTML
+		if ($oRenderingOutput->GetHtml() !== '')
+		{
+			if ($sMode === static::ENUM_RENDER_MODE_EXPLODED)
+			{
+				$output['html'] = $oRenderingOutput->GetHtml();
+			}
+			else
+			{
+				$output['html'] .= $oRenderingOutput->GetHtml();
+			}
+		}
 
-        // HTML
-        if ($oRenderingOutput->GetHtml() !== '')
-        {
-            if ($sMode === static::ENUM_RENDER_MODE_EXPLODED)
-            {
-                $output['html'] = $oRenderingOutput->GetHtml();
-            }
-            else
-            {
-                $output['html'] .= $oRenderingOutput->GetHtml();
-            }
-        }
+		// JS files
+		foreach ($oRenderingOutput->GetJsFiles() as $sJsFile)
+		{
+			if ($sMode === static::ENUM_RENDER_MODE_EXPLODED)
+			{
+				if (!in_array($sJsFile, $output['js_files']))
+				{
+					$output['js_files'][] = $sJsFile;
+				}
+			}
+			else
+			{
+				$output['html'] .= '<script src="' . $sJsFile . '" type="text/javascript"></script>';
+			}
+		}
+		// JS inline
+		if ($oRenderingOutput->GetJs() !== '')
+		{
+			if ($sMode === static::ENUM_RENDER_MODE_EXPLODED)
+			{
+				$output['js_inline'] .= ' ' . $oRenderingOutput->GetJs();
+			}
+			else
+			{
+				$output['html'] .= '<script type="text/javascript">' . $oRenderingOutput->GetJs() . '</script>';
+			}
+		}
 
-        // JS files
-        foreach ($oRenderingOutput->GetJsFiles() as $sJsFile)
-        {
-            if ($sMode === static::ENUM_RENDER_MODE_EXPLODED)
-            {
-                if (!in_array($sJsFile, $output['js_files']))
-                {
-                    $output['js_files'][] = $sJsFile;
-                }
-            }
-            else
-            {
-                $output['html'] .= '<script src="' . $sJsFile . '" type="text/javascript"></script>';
-            }
-        }
-        // JS inline
-        if ($oRenderingOutput->GetJs() !== '')
-        {
-            if ($sMode === static::ENUM_RENDER_MODE_EXPLODED)
-            {
-                $output['js_inline'] .= ' ' . $oRenderingOutput->GetJs();
-            }
-            else
-            {
-                $output['html'] .= '<script type="text/javascript">' . $oRenderingOutput->GetJs() . '</script>';
-            }
-        }
+		// CSS files
+		foreach ($oRenderingOutput->GetCssFiles() as $sCssFile)
+		{
+			if ($sMode === static::ENUM_RENDER_MODE_EXPLODED)
+			{
+				if (!in_array($sCssFile, $output['css_files']))
+				{
+					$output['css_files'][] = $sCssFile;
+				}
+			}
+			else
+			{
+				$output['html'] .= '<link href="' . $sCssFile . '" rel="stylesheet" />';
+			}
+		}
+		// CSS inline
+		if ($oRenderingOutput->GetCss() !== '')
+		{
+			if ($sMode === static::ENUM_RENDER_MODE_EXPLODED)
+			{
+				$output['css_inline'] .= ' ' . $oRenderingOutput->GetCss();
+			}
+			else
+			{
+				$output['html'] .= '<style>' . $oRenderingOutput->GetCss() . '</style>';
+			}
+		}
 
-        // CSS files
-        foreach ($oRenderingOutput->GetCssFiles() as $sCssFile)
-        {
-            if ($sMode === static::ENUM_RENDER_MODE_EXPLODED)
-            {
-                if (!in_array($sCssFile, $output['css_files']))
-                {
-                    $output['css_files'][] = $sCssFile;
-                }
-            }
-            else
-            {
-                $output['html'] .= '<link href="' . $sCssFile . '" rel="stylesheet" />';
-            }
-        }
-        // CSS inline
-        if ($oRenderingOutput->GetCss() !== '')
-        {
-            if ($sMode === static::ENUM_RENDER_MODE_EXPLODED)
-            {
-                $output['css_inline'] .= ' ' . $oRenderingOutput->GetCss();
-            }
-            else
-            {
-                $output['html'] .= '<style>' . $oRenderingOutput->GetCss() . '</style>';
-            }
-        }
+		return $output;
+	}
 
-        return $output;
-    }
 }

+ 148 - 148
sources/renderer/renderingoutput.class.inc.php

@@ -26,153 +26,153 @@ namespace Combodo\iTop\Renderer;
  */
 class RenderingOutput
 {
-    protected $sHtml;
-    protected $sJsInline;
-    protected $aJsFiles;
-    protected $sCssInline;
-    protected $aCssFiles;
-
-    public function __construct()
-    {
-        $this->sHtml = '';
-        $this->sJsInline = '';
-        $this->aJsFiles = array();
-        $this->sCssInline = '';
-        $this->aCssFiles = array();
-    }
-
-    /**
-     *
-     * @return string
-     */
-    public function GetHtml()
-    {
-        return $this->sHtml;
-    }
-
-    /**
-     *
-     * @return string
-     */
-    public function GetJs()
-    {
-        return $this->sJsInline;
-    }
-
-    /**
-     *
-     * @return array
-     */
-    public function GetJsFiles()
-    {
-        return $this->aJsFiles;
-    }
-
-    /**
-     *
-     * @return string
-     */
-    public function GetCss()
-    {
-        return $this->sCssInline;
-    }
-
-    /**
-     *
-     * @return array
-     */
-    public function GetCssFiles()
-    {
-        return $this->aCssFiles;
-    }
-
-    /**
-     *
-     * @param string $sHtml
-     * @return \Combodo\iTop\Renderer\RenderingOutput
-     */
-    public function AddHtml($sHtml)
-    {
-        $this->sHtml .= $sHtml;
-        return $this;
-    }
-
-    /**
-     *
-     * @param string $sJs
-     * @return \Combodo\iTop\Renderer\RenderingOutput
-     */
-    public function AddJs($sJs)
-    {
-        $this->sJsInline .= $sJs . "\n";
-        return $this;
-    }
-
-    /**
-     *
-     * @param string $sFile
-     * @return \Combodo\iTop\Renderer\RenderingOutput
-     */
-    public function AddJsFile($sFile)
-    {
-        if (!in_array($sFile, $this->aJsFiles))
-        {
-            $this->aJsFiles[] = $sFile;
-        }
-        return $this;
-    }
-
-    /**
-     *
-     * @param string $sFile
-     * @return \Combodo\iTop\Renderer\RenderingOutput
-     */
-    public function RemoveJsFile($sFile)
-    {
-        if (in_array($sFile, $this->aJsFiles))
-        {
-            unset($this->aJsFiles[$sFile]);
-        }
-        return $this;
-    }
-
-    /**
-     *
-     * @param string $sCss
-     * @return \Combodo\iTop\Renderer\RenderingOutput
-     */
-    public function AddCss($sCss)
-    {
-        $this->sCssInline .= $sCss . "\n";
-        return $this;
-    }
-
-    /**
-     *
-     * @param string $sFile
-     * @return \Combodo\iTop\Renderer\RenderingOutput
-     */
-    public function AddCssFile($sFile)
-    {
-        if (!in_array($sFile, $this->aCssFiles))
-        {
-            $this->aCssFiles[] = $sFile;
-        }
-        return $this;
-    }
-
-    /**
-     *
-     * @param string $sFile
-     * @return \Combodo\iTop\Renderer\RenderingOutput
-     */
-    public function RemoveCssFile($sFile)
-    {
-        if (in_array($sFile, $this->aCssFiles))
-        {
-            unset($this->aCssFiles[$sFile]);
-        }
-        return $this;
-    }
+	protected $sHtml;
+	protected $sJsInline;
+	protected $aJsFiles;
+	protected $sCssInline;
+	protected $aCssFiles;
+
+	public function __construct()
+	{
+		$this->sHtml = '';
+		$this->sJsInline = '';
+		$this->aJsFiles = array();
+		$this->sCssInline = '';
+		$this->aCssFiles = array();
+	}
+
+	/**
+	 *
+	 * @return string
+	 */
+	public function GetHtml()
+	{
+		return $this->sHtml;
+	}
+
+	/**
+	 *
+	 * @return string
+	 */
+	public function GetJs()
+	{
+		return $this->sJsInline;
+	}
+
+	/**
+	 *
+	 * @return array
+	 */
+	public function GetJsFiles()
+	{
+		return $this->aJsFiles;
+	}
+
+	/**
+	 *
+	 * @return string
+	 */
+	public function GetCss()
+	{
+		return $this->sCssInline;
+	}
+
+	/**
+	 *
+	 * @return array
+	 */
+	public function GetCssFiles()
+	{
+		return $this->aCssFiles;
+	}
+
+	/**
+	 *
+	 * @param string $sHtml
+	 * @return \Combodo\iTop\Renderer\RenderingOutput
+	 */
+	public function AddHtml($sHtml)
+	{
+		$this->sHtml .= $sHtml;
+		return $this;
+	}
+
+	/**
+	 *
+	 * @param string $sJs
+	 * @return \Combodo\iTop\Renderer\RenderingOutput
+	 */
+	public function AddJs($sJs)
+	{
+		$this->sJsInline .= $sJs . "\n";
+		return $this;
+	}
+
+	/**
+	 *
+	 * @param string $sFile
+	 * @return \Combodo\iTop\Renderer\RenderingOutput
+	 */
+	public function AddJsFile($sFile)
+	{
+		if (!in_array($sFile, $this->aJsFiles))
+		{
+			$this->aJsFiles[] = $sFile;
+		}
+		return $this;
+	}
+
+	/**
+	 *
+	 * @param string $sFile
+	 * @return \Combodo\iTop\Renderer\RenderingOutput
+	 */
+	public function RemoveJsFile($sFile)
+	{
+		if (in_array($sFile, $this->aJsFiles))
+		{
+			unset($this->aJsFiles[$sFile]);
+		}
+		return $this;
+	}
+
+	/**
+	 *
+	 * @param string $sCss
+	 * @return \Combodo\iTop\Renderer\RenderingOutput
+	 */
+	public function AddCss($sCss)
+	{
+		$this->sCssInline .= $sCss . "\n";
+		return $this;
+	}
+
+	/**
+	 *
+	 * @param string $sFile
+	 * @return \Combodo\iTop\Renderer\RenderingOutput
+	 */
+	public function AddCssFile($sFile)
+	{
+		if (!in_array($sFile, $this->aCssFiles))
+		{
+			$this->aCssFiles[] = $sFile;
+		}
+		return $this;
+	}
+
+	/**
+	 *
+	 * @param string $sFile
+	 * @return \Combodo\iTop\Renderer\RenderingOutput
+	 */
+	public function RemoveCssFile($sFile)
+	{
+		if (in_array($sFile, $this->aCssFiles))
+		{
+			unset($this->aCssFiles[$sFile]);
+		}
+		return $this;
+	}
 
 }