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 + */ +class SubFormField extends Field +{ + protected $oForm; + + public function __construct($sId, $sParentFormId, Closure $onFinalizeCallback = null) + { + $this->oForm = new \Combodo\iTop\Form\Form($sParentFormId.'-subform_'.$sId); + parent::__construct($sId, $onFinalizeCallback); + } + + public function GetForm() + { + return $this->oForm; + } + + /** + * Checks the validators to see if the field's current value is valid. + * Then sets $bValid and $aErrorMessages. + * + * @return boolean + */ + public function Validate() + { + $this->oForm->Validate(); + } + + public function GetValid() + { + return $this->oForm->GetValid(); + } + + public function GetErrorMessages() + { + return $this->oForm->GetErrorMessages(); + } + + public function GetCurrentValue() + { + return $this->oForm->GetCurrentValues(); + } + + public function SetCurrentValue($value) + { + return $this->oForm->SetCurrentValues($value); + } +} diff --git a/sources/form/form.class.inc.php b/sources/form/form.class.inc.php index 83a63e078..587b7eae0 100644 --- a/sources/form/form.class.inc.php +++ b/sources/form/form.class.inc.php @@ -60,6 +60,25 @@ class Form 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. @@ -154,6 +173,7 @@ class Form public function AddField(Field $oField, $aDependsOnIds = array()) { + $oField->SetFormPath($this->sId); $this->aFields[$oField->GetId()] = $oField; return $this; } diff --git a/sources/form/formmanager.class.inc.php b/sources/form/formmanager.class.inc.php index 0a5eb6a9f..26c23795a 100644 --- a/sources/form/formmanager.class.inc.php +++ b/sources/form/formmanager.class.inc.php @@ -52,11 +52,17 @@ abstract class FormManager // Overload in child class when needed } + /** + * @return Form + */ public function GetForm() { return $this->oForm; } + /** + * @return FormRenderer + */ public function GetRenderer() { return $this->oRenderer; diff --git a/sources/renderer/console/consoleformrenderer.class.inc.php b/sources/renderer/console/consoleformrenderer.class.inc.php new file mode 100644 index 000000000..79da01cae --- /dev/null +++ b/sources/renderer/console/consoleformrenderer.class.inc.php @@ -0,0 +1,39 @@ + + +namespace Combodo\iTop\Renderer\Console; + +use Combodo\iTop\Form\Form; +use Combodo\iTop\Renderer\FormRenderer; +use Combodo\iTop\Renderer\RenderingOutput; +use \Dict; + +require_once('fieldrenderer/consolesimplefieldrenderer.class.inc.php'); +require_once('fieldrenderer/consolesubformfieldrenderer.class.inc.php'); + +class ConsoleFormRenderer extends FormRenderer +{ + const DEFAULT_RENDERER_NAMESPACE = 'Combodo\\iTop\\Renderer\\Console\\FieldRenderer\\'; + + public function __construct(Form $oForm) + { + parent::__construct($oForm); + $this->AddSupportedField('StringField', 'ConsoleSimpleFieldRenderer'); + $this->AddSupportedField('SubFormField', 'ConsoleSubFormFieldRenderer'); + } +} \ No newline at end of file diff --git a/sources/renderer/console/fieldrenderer/consolesimplefieldrenderer.class.inc.php b/sources/renderer/console/fieldrenderer/consolesimplefieldrenderer.class.inc.php new file mode 100644 index 000000000..3dc862aba --- /dev/null +++ b/sources/renderer/console/fieldrenderer/consolesimplefieldrenderer.class.inc.php @@ -0,0 +1,139 @@ + + +namespace Combodo\iTop\Renderer\Console\FieldRenderer; + +use \Dict; +use Combodo\iTop\Renderer\FieldRenderer; +use Combodo\iTop\Renderer\RenderingOutput; + +class ConsoleSimpleFieldRenderer extends FieldRenderer +{ + public function Render() + { + $oOutput = new RenderingOutput(); + $sFieldClass = get_class($this->oField); + + // 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': + if ($this->oField->GetLabel() !== '') + { + $oOutput->AddHtml(''); + } + $oOutput->AddHtml(''); + $oOutput->AddHtml(''); + break; + } + } + // ... and in read-only mode + else + { + switch ($sFieldClass) + { + case 'Combodo\\iTop\\Form\\Field\\StringField': + if ($this->oField->GetLabel() !== '') + { + $oOutput->AddHtml(''); + } + $oOutput->AddHtml('
' . $this->oField->GetCurrentValue() . '
'); + break; + } + } + + switch ($sFieldClass) + { + case 'Combodo\\iTop\\Form\\Field\\StringField': + $oOutput->AddJs( +<<oField->GetGlobalId()}").off("change").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; + } + + // 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()) + ); + } + + $sValidators = json_encode($aValidators); + $sFormFieldOptions = +<<'); + oValidationElement.tooltip({ + items: 'span', + tooltipClass: 'form_field_error', + content: function() { + return $(this).find('img').attr('data-tooltip'); // As opposed to the default 'content' handler, do not escape the contents of 'title' + } + }); + } + } +} +EOF + ; + + 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( + << + +namespace Combodo\iTop\Renderer\Console\FieldRenderer; + +use \Dict; +use Combodo\iTop\Renderer\Console\ConsoleFormRenderer; +use Combodo\iTop\Renderer\FieldRenderer; +use Combodo\iTop\Renderer\RenderingOutput; + +class ConsoleSubFormFieldRenderer extends FieldRenderer +{ + public function Render() + { + $oOutput = new RenderingOutput(); + + $oOutput->AddHtml('
'); + $oOutput->AddHtml('
'); + + $oRenderer = new ConsoleFormRenderer($this->oField->GetForm()); + $aRenderRes = $oRenderer->Render(); + + $aFieldSetOptions = array( + 'fields_list' => $aRenderRes, + 'fields_impacts' => $this->oField->GetForm()->GetFieldsImpacts(), + 'form_path' => $this->oField->GetForm()->GetId() + ); + $sFieldSetOptions = json_encode($aFieldSetOptions); + $oOutput->AddJs( +<<oField->GetGlobalId()}").field_set($sFieldSetOptions); + $("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").subform_field({field_set: $("#fieldset_{$this->oField->GetGlobalId()}")}); +EOF + ); + return $oOutput; + } +} \ No newline at end of file diff --git a/sources/renderer/formrenderer.class.inc.php b/sources/renderer/formrenderer.class.inc.php index ba86ee018..2481ba23b 100644 --- a/sources/renderer/formrenderer.class.inc.php +++ b/sources/renderer/formrenderer.class.inc.php @@ -19,6 +19,7 @@ namespace Combodo\iTop\Renderer; +use \Exception; use \Dict; use \Combodo\iTop\Form\Form; @@ -95,8 +96,7 @@ abstract class FormRenderer } else { - // TODO : We might want to throw an exception. - return null; + throw new Exception('Field type not supported by the renderer: '.get_class($oField)); } } @@ -104,7 +104,7 @@ abstract class FormRenderer * Returns the field identified by the id $sId in $this->oForm. * * @param string $sId - * @return Combodo\iTop\Renderer\FieldRenderer + * @return \Combodo\iTop\Renderer\FieldRenderer */ public function GetFieldRendererClassFromId($sId) { @@ -164,7 +164,7 @@ abstract class FormRenderer * 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 \Combodo\iTop\Form\Field\Field $oField * @param string $sMode 'exploded'|'joined' * @return mixed */ @@ -180,81 +180,78 @@ abstract class FormRenderer ); $sFieldRendererClass = $this->GetFieldRendererClass($oField); - // TODO : We might want to throw an exception instead when there is no renderer for that field - if ($sFieldRendererClass !== null) + + $oFieldRenderer = new $sFieldRendererClass($oField); + $oFieldRenderer->SetEndpoint($this->GetEndpoint()); + + $oRenderingOutput = $oFieldRenderer->Render(); + + // HTML + if ($oRenderingOutput->GetHtml() !== '') { - $oFieldRenderer = new $sFieldRendererClass($oField); - $oFieldRenderer->SetEndpoint($this->GetEndpoint()); - - $oRenderingOutput = $oFieldRenderer->Render(); - - // HTML - if ($oRenderingOutput->GetHtml() !== '') + if ($sMode === static::ENUM_RENDER_MODE_EXPLODED) { - 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['html'] = $oRenderingOutput->GetHtml(); - } - else - { - $output['html'] .= $oRenderingOutput->GetHtml(); + $output['js_files'][] = $sJsFile; } } + else + { + $output['html'] .= ''; + } + } + // JS inline + if ($oRenderingOutput->GetJs() !== '') + { + if ($sMode === static::ENUM_RENDER_MODE_EXPLODED) + { + $output['js_inline'] .= ' ' . $oRenderingOutput->GetJs(); + } + else + { + $output['html'] .= ''; + } + } - // JS files - foreach ($oRenderingOutput->GetJsFiles() as $sJsFile) + // CSS files + foreach ($oRenderingOutput->GetCssFiles() as $sCssFile) + { + if ($sMode === static::ENUM_RENDER_MODE_EXPLODED) { - if ($sMode === static::ENUM_RENDER_MODE_EXPLODED) + if (!in_array($sCssFile, $output['css_files'])) { - if (!in_array($sJsFile, $output['js_files'])) - { - $output['js_files'][] = $sJsFile; - } - } - else - { - $output['html'] .= ''; + $output['css_files'][] = $sCssFile; } } - // JS inline - if ($oRenderingOutput->GetJs() !== '') + else { - if ($sMode === static::ENUM_RENDER_MODE_EXPLODED) - { - $output['js_inline'] .= ' ' . $oRenderingOutput->GetJs(); - } - else - { - $output['html'] .= ''; - } + $output['html'] .= ''; } - - // CSS files - foreach ($oRenderingOutput->GetCssFiles() as $sCssFile) + } + // CSS inline + if ($oRenderingOutput->GetCss() !== '') + { + if ($sMode === static::ENUM_RENDER_MODE_EXPLODED) { - if ($sMode === static::ENUM_RENDER_MODE_EXPLODED) - { - if (!in_array($sCssFile, $output['css_files'])) - { - $output['css_files'][] = $sCssFile; - } - } - else - { - $output['html'] .= ''; - } + $output['css_inline'] .= ' ' . $oRenderingOutput->GetCss(); } - // CSS inline - if ($oRenderingOutput->GetCss() !== '') + else { - if ($sMode === static::ENUM_RENDER_MODE_EXPLODED) - { - $output['css_inline'] .= ' ' . $oRenderingOutput->GetCss(); - } - else - { - $output['html'] .= ''; - } + $output['html'] .= ''; } }