UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

682 lines (588 loc) 20.7 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2004-2009 1&1 Internet AG, Germany, http://www.1und1.de License: MIT: https://opensource.org/licenses/MIT See the LICENSE file in the project's top-level directory for details. Authors: * Martin Wittemann (martinwittemann) ************************************************************************ */ /** * This validation manager is responsible for validation of forms. * * @ignore(qx.ui.tooltip) * @ignore(qx.ui.tooltip.Manager.*) */ qx.Class.define("qx.ui.form.validation.Manager", { extend : qx.core.Object, construct : function() { this.base(arguments); // storage for all form items this.__formItems = []; // storage for all results of async validation calls this.__asyncResults = {}; // set the default required field message this.setRequiredFieldMessage(qx.locale.Manager.tr("This field is required")); }, events : { /** * Change event for the valid state. */ "changeValid" : "qx.event.type.Data", /** * Signals that the validation is done. This is not needed on synchronous * validation (validation is done right after the call) but very important * in the case an asynchronous validator will be used. */ "complete" : "qx.event.type.Event" }, properties : { /** * The validator of the form itself. You can set a function (for * synchronous validation) or a {@link qx.ui.form.validation.AsyncValidator}. * In both cases, the function can have all added form items as first * argument and the manager as a second argument. The manager should be used * to set the {@link #invalidMessage}. * * Keep in mind that the validator is optional if you don't need the * validation in the context of the whole form. * @type {Function | AsyncValidator} */ validator : { check : "value instanceof Function || qx.Class.isSubClassOf(value.constructor, qx.ui.form.validation.AsyncValidator)", init : null, nullable : true }, /** * The invalid message should store the message why the form validation * failed. It will be added to the array returned by * {@link #getInvalidMessages}. */ invalidMessage : { check : "String", init: "" }, /** * This message will be shown if a required field is empty and no individual * {@link qx.ui.form.MForm#requiredInvalidMessage} is given. */ requiredFieldMessage : { check : "String", init : "" }, /** * The context for the form validation. */ context : { nullable : true } }, members : { __formItems : null, __valid : null, __asyncResults : null, __syncValid : null, /** * Add a form item to the validation manager. * * The form item has to implement at least two interfaces: * <ol> * <li>The {@link qx.ui.form.IForm} Interface</li> * <li>One of the following interfaces: * <ul> * <li>{@link qx.ui.form.IBooleanForm}</li> * <li>{@link qx.ui.form.IColorForm}</li> * <li>{@link qx.ui.form.IDateForm}</li> * <li>{@link qx.ui.form.INumberForm}</li> * <li>{@link qx.ui.form.IStringForm}</li> * </ul> * </li> * </ol> * The validator can be a synchronous or asynchronous validator. In * both cases the validator can either returns a boolean or fire an * {@link qx.core.ValidationError}. For synchronous validation, a plain * JavaScript function should be used. For all asynchronous validations, * a {@link qx.ui.form.validation.AsyncValidator} is needed to wrap the * plain function. * * @param formItem {qx.ui.core.Widget} The form item to add. * @param validator {Function | qx.ui.form.validation.AsyncValidator} * The validator. * @param context {var?null} The context of the validator. */ add: function(formItem, validator, context) { // check for the form API if (!this.__supportsInvalid(formItem)) { throw new Error("Added widget not supported."); } // check for the data type if (this.__supportsSingleSelection(formItem) && !formItem.getValue) { // check for a validator if (validator != null) { throw new Error("Widgets supporting selection can only be validated " + "in the form validator"); } } var dataEntry = { item : formItem, validator : validator, valid : null, context : context }; this.__formItems.push(dataEntry); }, /** * Remove a form item from the validation manager. * * @param formItem {qx.ui.core.Widget} The form item to remove. * @return {qx.ui.core.Widget?null} The removed form item or * <code>null</code> if the item could not be found. */ remove : function(formItem) { var items = this.__formItems; for (var i = 0, len = items.length; i < len; i++) { if (formItem === items[i].item) { items.splice(i, 1); return formItem; } } return null; }, /** * Returns registered form items from the validation manager. * * @return {Array} The form items which will be validated. */ getItems : function() { var items = []; for (var i=0; i < this.__formItems.length; i++) { items.push(this.__formItems[i].item); }; return items; }, /** * Invokes the validation. If only synchronous validators are set, the * result of the whole validation is available at the end of the method * and can be returned. If an asynchronous validator is set, the result * is still unknown at the end of this method so nothing will be returned. * In both cases, a {@link #complete} event will be fired if the validation * has ended. The result of the validation can then be accessed with the * {@link #getValid} method. * * @return {Boolean|undefined} The validation result, if available. */ validate : function() { var valid = true; this.__syncValid = true; // collaboration of all synchronous validations var items = []; // check all validators for the added form items for (var i = 0; i < this.__formItems.length; i++) { var formItem = this.__formItems[i].item; var validator = this.__formItems[i].validator; // store the items in case of form validation items.push(formItem); // ignore all form items without a validator if (validator == null) { // check for the required property var validatorResult = this._validateRequired(formItem); valid = valid && validatorResult; this.__syncValid = validatorResult && this.__syncValid; continue; } var validatorResult = this._validateItem( this.__formItems[i], formItem.getValue() ); // keep that order to ensure that null is returned on async cases valid = validatorResult && valid; if (validatorResult != null) { this.__syncValid = validatorResult && this.__syncValid; } } // check the form validator (be sure to invoke it even if the form // items are already false, so keep the order!) var formValid = this.__validateForm(items); if (qx.lang.Type.isBoolean(formValid)) { this.__syncValid = formValid && this.__syncValid; } valid = formValid && valid; this._setValid(valid); if (qx.lang.Object.isEmpty(this.__asyncResults)) { this.fireEvent("complete"); } return valid; }, /** * Checks if the form item is required. If so, the value is checked * and the result will be returned. If the form item is not required, true * will be returned. * * @param formItem {qx.ui.core.Widget} The form item to check. * @return {var} Validation result */ _validateRequired : function(formItem) { if (formItem.getRequired()) { var validatorResult; // if its a widget supporting the selection if (this.__supportsSingleSelection(formItem)) { validatorResult = !!formItem.getSelection()[0]; } else if (this.__supportsDataBindingSelection(formItem)) { validatorResult = (formItem.getSelection().getLength() > 0); } else { var value = formItem.getValue(); validatorResult = !!value || value === 0; } formItem.setValid(validatorResult); var individualMessage = formItem.getRequiredInvalidMessage(); var message = individualMessage ? individualMessage : this.getRequiredFieldMessage(); formItem.setInvalidMessage(message); return validatorResult; } return true; }, /** * Validates a form item. This method handles the differences of * synchronous and asynchronous validation and returns the result of the * validation if possible (synchronous cases). If the validation is * asynchronous, null will be returned. * * @param dataEntry {Object} The map stored in {@link #add} * @param value {var} The currently set value * @return {Boolean|null} Validation result or <code>null</code> for async * validation */ _validateItem : function(dataEntry, value) { var formItem = dataEntry.item; var context = dataEntry.context; var validator = dataEntry.validator; // check for asynchronous validation if (this.__isAsyncValidator(validator)) { // used to check if all async validations are done this.__asyncResults[formItem.toHashCode()] = null; validator.validate(formItem, formItem.getValue(), this, context); return null; } var validatorResult = null; try { var validatorResult = validator.call(context || this, value, formItem); if (validatorResult === undefined) { validatorResult = true; } } catch (e) { if (e instanceof qx.core.ValidationError) { validatorResult = false; if (e.message && e.message != qx.type.BaseError.DEFAULTMESSAGE) { var invalidMessage = e.message; } else { var invalidMessage = e.getComment(); } formItem.setInvalidMessage(invalidMessage); } else { throw e; } } formItem.setValid(validatorResult); dataEntry.valid = validatorResult; return validatorResult; }, /** * Validates the form. It checks for asynchronous validation and handles * the differences to synchronous validation. If no form validator is given, * true will be returned. If a synchronous validator is given, the * validation result will be returned. In asynchronous cases, null will be * returned cause the result is not available. * * @param items {qx.ui.core.Widget[]} An array of all form items. * @return {Boolean|null} description */ __validateForm: function(items) { var formValidator = this.getValidator(); var context = this.getContext() || this; if (formValidator == null) { return true; } // reset the invalidMessage this.setInvalidMessage(""); if (this.__isAsyncValidator(formValidator)) { this.__asyncResults[this.toHashCode()] = null; formValidator.validateForm(items, this, context); return null; } try { var formValid = formValidator.call(context, items, this); if (formValid === undefined) { formValid = true; } } catch (e) { if (e instanceof qx.core.ValidationError) { formValid = false; if (e.message && e.message != qx.type.BaseError.DEFAULTMESSAGE) { var invalidMessage = e.message; } else { var invalidMessage = e.getComment(); } this.setInvalidMessage(invalidMessage); } else { throw e; } } return formValid; }, /** * Helper function which checks, if the given validator is synchronous * or asynchronous. * * @param validator {Function|qx.ui.form.validation.AsyncValidator} * The validator to check. * @return {Boolean} True, if the given validator is asynchronous. */ __isAsyncValidator : function(validator) { var async = false; if (!qx.lang.Type.isFunction(validator)) { async = qx.Class.isSubClassOf( validator.constructor, qx.ui.form.validation.AsyncValidator ); } return async; }, /** * Returns true, if the given item implements the {@link qx.ui.form.IForm} * interface. * * @param formItem {qx.core.Object} The item to check. * @return {Boolean} true, if the given item implements the * necessary interface. */ __supportsInvalid : function(formItem) { var clazz = formItem.constructor; return qx.Class.hasInterface(clazz, qx.ui.form.IForm); }, /** * Returns true, if the given item implements the * {@link qx.ui.core.ISingleSelection} interface. * * @param formItem {qx.core.Object} The item to check. * @return {Boolean} true, if the given item implements the * necessary interface. */ __supportsSingleSelection : function(formItem) { var clazz = formItem.constructor; return qx.Class.hasInterface(clazz, qx.ui.core.ISingleSelection); }, /** * Returns true, if the given item implements the * {@link qx.data.controller.ISelection} interface. * * @param formItem {qx.core.Object} The item to check. * @return {Boolean} true, if the given item implements the * necessary interface. */ __supportsDataBindingSelection : function(formItem) { var clazz = formItem.constructor; return qx.Class.hasInterface(clazz, qx.data.controller.ISelection); }, /** * Sets the valid state of the manager. It generates the event if * necessary and stores the new value. * * @param value {Boolean|null} The new valid state of the manager. */ _setValid: function(value) { this._showToolTip(value); var oldValue = this.__valid; this.__valid = value; // check for the change event if (oldValue != value) { this.fireDataEvent("changeValid", value, oldValue); } }, /** * Responsible for showing a tooltip in case the validation is done for * widgets based on qx.ui.core.Widget. * @param valid {Boolean} <code>false</code>, if the tooltip should be shown */ _showToolTip : function(valid) { // ignore if we don't have a tooltip manager e.g. mobile apps if (!qx.ui.tooltip || !qx.ui.tooltip.Manager) { return; } var tooltip = qx.ui.tooltip.Manager.getInstance().getSharedErrorTooltip(); if (!valid) { var firstInvalid; for (var i = 0; i < this.__formItems.length; i++) { var item = this.__formItems[i].item; if (!item.isValid()) { firstInvalid = item; // only for desktop widgets if (!(item.getContentLocation)) { return; } // only consider items on the screen if (item.isSeeable() === false) { continue; } tooltip.setLabel(item.getInvalidMessage()); if (tooltip.getPlaceMethod() == "mouse") { var location = item.getContentLocation(); var top = location.top - tooltip.getOffsetTop(); tooltip.placeToPoint({left: location.right, top: top}); } else { tooltip.placeToWidget(item); } tooltip.show(); return; } } } else { tooltip.exclude(); } }, /** * Returns the valid state of the manager. * * @return {Boolean|null} The valid state of the manager. */ getValid: function() { return this.__valid; }, /** * Returns the valid state of the manager. * * @return {Boolean|null} The valid state of the manager. */ isValid: function() { return this.getValid(); }, /** * Returns an array of all invalid messages of the invalid form items and * the form manager itself. * * @return {String[]} All invalid messages. */ getInvalidMessages: function() { var messages = []; // combine the messages of all form items for (var i = 0; i < this.__formItems.length; i++) { var formItem = this.__formItems[i].item; if (!formItem.getValid()) { messages.push(formItem.getInvalidMessage()); } } // add the forms fail message if (this.getInvalidMessage() != "") { messages.push(this.getInvalidMessage()); } return messages; }, /** * Selects invalid form items * * @return {Array} invalid form items */ getInvalidFormItems : function() { var res = []; for (var i = 0; i < this.__formItems.length; i++) { var formItem = this.__formItems[i].item; if (!formItem.getValid()) { res.push(formItem); } } return res; }, /** * Resets the validator. */ reset: function() { // reset all form items for (var i = 0; i < this.__formItems.length; i++) { var dataEntry = this.__formItems[i]; // set the field to valid dataEntry.item.setValid(true); } // set the manager to its initial valid value this.__valid = null; this._showToolTip(true); }, /** * Internal helper method to set the given item to valid for asynchronous * validation calls. This indirection is used to determinate if the * validation process is completed or if other asynchronous validators * are still validating. {@link #__checkValidationComplete} checks if the * validation is complete and will be called at the end of this method. * * @param formItem {qx.ui.core.Widget} The form item to set the valid state. * @param valid {Boolean} The valid state for the form item. * * @internal */ setItemValid: function(formItem, valid) { // store the result this.__asyncResults[formItem.toHashCode()] = valid; formItem.setValid(valid); this.__checkValidationComplete(); }, /** * Internal helper method to set the form manager to valid for asynchronous * validation calls. This indirection is used to determinate if the * validation process is completed or if other asynchronous validators * are still validating. {@link #__checkValidationComplete} checks if the * validation is complete and will be called at the end of this method. * * @param valid {Boolean} The valid state for the form manager. * * @internal */ setFormValid : function(valid) { this.__asyncResults[this.toHashCode()] = valid; this.__checkValidationComplete(); }, /** * Checks if all asynchronous validators have validated so the result * is final and the {@link #complete} event can be fired. If that's not * the case, nothing will happen in the method. */ __checkValidationComplete : function() { var valid = this.__syncValid; // check if all async validators are done for (var hash in this.__asyncResults) { var currentResult = this.__asyncResults[hash]; valid = currentResult && valid; // the validation is not done so just do nothing if (currentResult == null) { return; } } // set the actual valid state of the manager this._setValid(valid); // reset the results this.__asyncResults = {}; // fire the complete event (no entry in the results with null) this.fireEvent("complete"); } }, /* ***************************************************************************** DESTRUCTOR ***************************************************************************** */ destruct : function() { this._showToolTip(true); this.__formItems = null; } });