diff --git a/js/form_field.js b/js/form_field.js new file mode 100644 index 000000000..81acc99ed --- /dev/null +++ b/js/form_field.js @@ -0,0 +1,205 @@ +//iTop Form field +; +$(function() +{ + // the widget definition, where 'itop' is the namespace, + // 'form_field' the widget name + $.widget( 'itop.form_field', + { + // default options + options: + { + validators: null + }, + + // the constructor + _create: function() + { + var me = this; + + this.element + .addClass('form_field'); + + this.element + .bind('field_change.form_field', function(event, data){ + me._onFieldChange(event, data); + }); + + this.element + .bind('set_validators.form_field', function(event, data){ + me.options.validators = data; + }); + + this.element + .bind('validate.form_field', function(event, data){ + return me.validate(); + }); + + this.element + .bind('set_current_value.form_field', function(event, data){ + return me.getCurrentValue(); + }); + }, + // 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 = {}; + + this.element.find(':input').each(function(index, elem){ + if($(elem).is(':hidden') || $(elem).is(':text') || $(elem).is('textarea')) + { + value[$(elem).attr('name')] = $(elem).val(); + } + else if($(elem).is('select')) + { + value[$(elem).attr('name')] = []; + $(elem).find('option:selected').each(function(){ + value[$(elem).attr('name')].push($(this).val()); + }); + } + else if($(elem).is(':checkbox') || $(elem).is(':radio')) + { + if(value[$(elem).attr('name')] === undefined) + { + value[$(elem).attr('name')] = []; + } + if($(elem).is(':checked')) + { + value[$(elem).attr('name')].push($(elem).val()); + } + } + else + { + console.log('Form field : Input type not handle yet.'); + } + }); + + return value; + }, + validate: function() + { + 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 none empty array, we have to check is 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) +'"'); + } + } + } + } + } + + // Rendering visual feedback on the field + this.element.removeClass('has-success has-warning has-error') + this.element.find('.help-block').html(''); + if(!oResult.is_valid) + { + this.element.addClass('has-error'); + for(var i in oResult.error_messages) + { + this.element.find('.help-block').append($('

' + oResult.error_messages[i] + '

')); + } + } + + return oResult; + }, + showOptions: function() + { + return this.options; + } + }); +}); diff --git a/js/form_handler.js b/js/form_handler.js new file mode 100644 index 000000000..4a0cb1612 --- /dev/null +++ b/js/form_handler.js @@ -0,0 +1,327 @@ +//iTop Form handler +; +$(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, + 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 + }, + + buildData: + { + script_code: '', + style_code: '' + }, + + // the constructor + _create: function() + { + var me = this; + + this.element + .addClass('form_handler'); + + this.element + .bind('field_change.form_handler', function(event, data){ + me._onFieldChange(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) + { + this.options.submit_btn_selector.off('click').on('click', this._onSubmitClick()); + } + if(this.options.cancel_btn_selector !== null) + { + this.options.cancel_btn_selector.off('click').on('click', this._onCancelClick()); + } + }, + + // 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() + { + 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+'"]').trigger('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; + }, + _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) + { + var me = this; + + // 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; + } + + // 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 + }, + this._onUpdateSuccess(data) + ) + .fail(this._onUpdateFailure(data)) + .always(this._onUpdateAlways()); + } + else + { + // Check self NOW as they are no ajax call + this.element.find('[' + this.options.field_identifier_attr + '="' + data.name + '"]').trigger('validate'); + } + }, + // Intended for overloading in derived classes + _onSubmitClick: function() + { + }, + // Intended for overloading in derived classes + _onCancelClick: function() + { + }, + // Intended for overloading in derived classes + _onUpdateSuccess: function(data) + { + 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_field[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); + } + }, + // Intended for overloading in derived classes + _onUpdateFailure: function(data) + { + }, + // Intended for overloading in derived classes + _onUpdateAlways: function() + { + // 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._enableFormAfterLoading(); + }, + // Intended for overloading in derived classes + _disableFormBeforeLoading: function() + { + }, + // Intended for overloading in derived classes + _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; + } + // JS widget itop.form_field + if (field.validators != undefined) + { + this.buildData.script_code += '; $("[' + this.options.field_identifier_attr + '=\'' + field.id + '\']").trigger(\'set_validators\', ' + JSON.stringify(field.validators) + ');'; + } + }, + showOptions: function() // Debug helper + { + console.log(this.options); + }, + 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); + } + }); +});