diff --git a/js/field_set.js b/js/field_set.js new file mode 100644 index 000000000..69409f077 --- /dev/null +++ b/js/field_set.js @@ -0,0 +1,305 @@ +//iTop Form handler +; +$(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 + }, + + 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); + }); + + // Creating DOM elements if not using user's specifics + if(this.options.script_element === null) + { + this.options.script_element = $(''); + this.element.after(this.options.script_element); + } + if(this.options.style_element === null) + { + this.options.style_element = $(''); + this.element.before(this.options.style_element); + } + + // 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(); + + 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(); + + // 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); + } + + // Validate the field + var oRes = this._getField(data.name).triggerHandler('validate', {touched_fields_only: true}); + if (!oRes.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(); + + 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); + } + + // 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); + + // 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(); + + this.options.is_valid = true; + + + 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', 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) + $('').appendTo('head'); + }, + _loadJsFile: function(url) + { + if (!$('script[src="'+url+'"]').length) + $.getScript(url); + }, + // Place a field for which no container exists + _addField: function(field_id) + { + $('
').appendTo(this.element); + }, + _prepareField: function(field_id) + { + var field = this.options.fields_list[field_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( $('') ); + } + else + { + this._addField(field.id); + } + + 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 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(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()); + } + }); +}); diff --git a/js/form_field.js b/js/form_field.js index 666786ab0..9a616a726 100644 --- a/js/form_field.js +++ b/js/form_field.js @@ -26,19 +26,21 @@ $(function() 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](); + return me[callback](event, data); } else if(typeof callback === 'function') { - return callback(me); + return callback(me, event, data); } else { @@ -72,29 +74,29 @@ $(function() }, getCurrentValue: function() { - var value = {}; + var value = null; this.element.find(':input').each(function(index, elem){ if($(elem).is(':hidden') || $(elem).is(':text') || $(elem).is('textarea')) { - value[$(elem).attr('name')] = $(elem).val(); + value = $(elem).val(); } else if($(elem).is('select')) { - value[$(elem).attr('name')] = []; + value = []; $(elem).find('option:selected').each(function(){ - value[$(elem).attr('name')].push($(this).val()); + value.push($(this).val()); }); } else if($(elem).is(':checkbox') || $(elem).is(':radio')) { - if(value[$(elem).attr('name')] === undefined) + if(value === null) { - value[$(elem).attr('name')] = []; + value = []; } if($(elem).is(':checked')) { - value[$(elem).attr('name')].push($(elem).val()); + value.push($(elem).val()); } } else @@ -105,10 +107,10 @@ $(function() return value; }, - validate: function() + validate: function(event, data) { var oResult = { is_valid: true, error_messages: [] }; - + // Doing data validation if(this.options.validators !== null) { @@ -139,7 +141,7 @@ $(function() oResult.is_valid = false; oResult.error_messages.push(oValidator.message); } - // ... In case of none empty array, we have to check is the value is not null + // ... 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) @@ -190,7 +192,7 @@ $(function() } } - this.options.on_validation_callback(); + this.options.on_validation_callback(this, oResult); return oResult; }, diff --git a/js/form_handler.js b/js/form_handler.js index 9e701ccb7..d2947139f 100644 --- a/js/form_handler.js +++ b/js/form_handler.js @@ -11,25 +11,13 @@ $(function() { formmanager_class: null, formmanager_data: null, - 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: [], submit_btn_selector: null, cancel_btn_selector: null, endpoint: null, is_modal: false, - is_valid: true, - script_element: null, - style_element: null + field_set: null }, - buildData: - { - script_code: '', - style_code: '' - }, - // the constructor _create: function() { @@ -37,30 +25,11 @@ $(function() this.element .addClass('form_handler'); - - this.element - .bind('field_change', function(event, data){ - me._onFieldChange(event, data); + + this.element.bind('update_fields', function(event, data){ + this._onUpdateFields(event, data); }); - - // Creating DOM elements if not using user's specifics - if(this.options.script_element === null) - { - this.options.script_element = $(''); - this.element.after(this.options.script_element); - } - if(this.options.style_element === null) - { - this.options.style_element = $(''); - this.element.before(this.options.style_element); - } - - // Building the form - if(this.options.fields_list !== null) - { - this.buildForm(); - } - + // Binding buttons if(this.options.submit_btn_selector !== null) { @@ -97,41 +66,13 @@ $(function() }, getCurrentValues: function() { - var result = {}; - - for(var i in this.options.fields_list) - { - var field = this.options.fields_list[i]; - if(this.element.find('[' + this.options.field_identifier_attr + '="'+field.id+'"]').hasClass('form_field')) - { - $.extend(true, result, this.element.find('[' + this.options.field_identifier_attr + '="'+field.id+'"]').triggerHandler('get_current_value')); - } - else - { - console.log('Form handler : 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; + return this.options.field_set.triggerHandler('get_current_values'); }, - _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) + _onUpdateFields: function(event, data) { var me = this; - + var sFormPath = data.form_path; + // Data checks if(this.options.endpoint === null) { @@ -149,37 +90,23 @@ $(function() return false; } - // 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); - } - - var requestedFields = this._getRequestedFields(data.name); - if(requestedFields.length > 0) - { - 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: requestedFields - }, - function(data){ - me._onUpdateSuccess(data); - } - ) - .fail(function(data){ me._onUpdateFailure(data); }) - .always(function(data){ me._onUpdateAlways(data); }); - } - else - { - // Check self NOW as they are no ajax call - this.element.find('[' + this.options.field_identifier_attr + '="' + data.name + '"]').trigger('validate'); - } + 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) @@ -190,40 +117,22 @@ $(function() { }, // Intended for overloading in derived classes - _onUpdateSuccess: function(data) + _onUpdateSuccess: function(data, sFormPath) { if(data.form.updated_fields !== undefined) { - this.buildData.script_code = ''; - this.buildData.style_code = ''; - - for (var i in data.form.updated_fields) - { - var updated_field = data.form.updated_fields[i]; - this.options.fields_list[updated_field.id] = updated_field; - this._prepareField(updated_field.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); - - // Evaluating script code as adding it to dom did not executed it (only script from update !) - eval(this.buildData.script_code); + 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) + _onUpdateFailure: function(data, sFormPath) { }, // Intended for overloading in derived classes - _onUpdateAlways: function(data) + _onUpdateAlways: function(data, sFormPath) { // Check all touched AFTER ajax is complete, otherwise the renderer will redraw the field in the mean time. - for(var i in this.options.touched_fields) - { - this.element.find('[' + this.options.field_identifier_attr + '="' + this.options.touched_fields[i] + '"]').trigger('validate'); - } + this.element.find('[data-form-path="'+sFormPath+'"]').trigger('validate'); this._enableFormAfterLoading(); }, // Intended for overloading in derived classes @@ -234,91 +143,6 @@ $(function() _enableFormAfterLoading: function() { }, - _loadCssFile: function(url) - { - if (!$('link[href="'+url+'"]').length) - $('').appendTo('head'); - }, - _loadJsFile: function(url) - { - if (!$('script[src="'+url+'"]').length) - $.getScript(url); - }, - // Place a field for which no container exists - _addField: function(field_id) - { - $('').appendTo(this.element); - }, - _prepareField: function(field_id) - { - var field = this.options.fields_list[field_id]; - - if(this.element.find('[' + this.options.field_identifier_attr + '="'+field.id+'"]').length === 1) - { - // We replace the node instead of just replacing the inner html so the previous widget is automatically destroyed. - this.element.find('[' + this.options.field_identifier_attr + '="'+field.id+'"]').replaceWith( $('') ); - } - else - { - this._addField(field.id); - } - - var field_container = this.element.find('[' + this.options.field_identifier_attr + '="'+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 field = this.options.fields_list[i]; - if(field.id === undefined) - { - console.log('Form handler : An field must have at least an id property.'); - return false; - } - - 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()); - }, showOptions: function() // Debug helper { console.log(this.options); diff --git a/js/subform_field.js b/js/subform_field.js new file mode 100644 index 000000000..1d185b7ac --- /dev/null +++ b/js/subform_field.js @@ -0,0 +1,46 @@ +//iTop Form 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 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(); + }, + 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: [] + } + }, + }); +}); diff --git a/sources/autoload.php b/sources/autoload.php index 0479d5111..d28d9e331 100644 --- a/sources/autoload.php +++ b/sources/autoload.php @@ -23,6 +23,7 @@ require_once APPROOT . 'sources/form/form.class.inc.php'; require_once APPROOT . 'sources/form/formmanager.class.inc.php'; require_once APPROOT . 'sources/form/field/field.class.inc.php'; +require_once APPROOT . 'sources/form/field/subformfield.class.inc.php'; require_once APPROOT . 'sources/form/field/textfield.class.inc.php'; require_once APPROOT . 'sources/form/field/hiddenfield.class.inc.php'; require_once APPROOT . 'sources/form/field/stringfield.class.inc.php'; diff --git a/sources/form/field/field.class.inc.php b/sources/form/field/field.class.inc.php index 3290cb45e..67390c5aa 100644 --- a/sources/form/field/field.class.inc.php +++ b/sources/form/field/field.class.inc.php @@ -36,6 +36,8 @@ abstract class Field const DEFAULT_VALID = true; protected $sId; + protected $sGlobalId; + protected $sFormPath; protected $sLabel; protected $bReadOnly; protected $bMandatory; @@ -52,6 +54,7 @@ abstract class Field 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; @@ -61,11 +64,33 @@ abstract class Field $this->onFinalizeCallback = $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; @@ -184,6 +209,14 @@ abstract class Field 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; @@ -206,7 +239,7 @@ abstract class Field * Note : Function is protected as aErrorMessages should not be add from outside * * @param string $sErrorMessage - * @return \Combodo\iTop\Field\Field + * @return \Combodo\iTop\Form\Field\Field */ protected function AddErrorMessage($sErrorMessage) { @@ -217,7 +250,7 @@ abstract class Field /** * Note : Function is protected as aErrorMessages should not be set from outside * - * @return \Combodo\iTop\Field\Field + * @return \Combodo\iTop\Form\Field\Field */ protected function EmptyErrorMessages() { @@ -236,7 +269,7 @@ abstract class Field { // 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(); + $callback($this); } } diff --git a/sources/form/field/subformfield.class.inc.php b/sources/form/field/subformfield.class.inc.php new file mode 100644 index 000000000..f45806c2b --- /dev/null +++ b/sources/form/field/subformfield.class.inc.php @@ -0,0 +1,76 @@ + + +namespace Combodo\iTop\Form\Field; + +use \Closure; +use \Combodo\iTop\Form\Field\Field; +use \Combodo\iTop\Form\Form; + +/** + * Description of StringField + * + * @author Guillaume Lajarige