Source: forms/controls/NumberSpinner.js

/**
 * Copyright (C) 2005-2016 Alfresco Software Limited.
 *
 * This file is part of Alfresco
 *
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * <p>This control is for the entry of numbers, while additionally allowing
 * "spinning" to adjust the number value (by scrolling or spinner-buttons).</p>
 *
 * @example <caption>Minimum configuration</caption>
 * {
 *    name: "alfresco/forms/controls/NumberSpinner", 
 *    config: {
 *       name: "one"
 *    }
 * }
 *
 * @example <caption>Full configuration</caption>
 * {
 *    name: "alfresco/forms/controls/NumberSpinner", 
 *    config: {
 *       name: "two",
 *       value: 3,
 *       min: 5,
 *       max: 10,
 *       requirementConfig: {
 *          initialValue: true
 *       },
 *       permitEmpty: true,
 *       permittedDecimalPlaces: 1
 *    }
 * },
 * 
 * @module alfresco/forms/controls/NumberSpinner
 * @extends module:alfresco/forms/controls/BaseFormControl
 * @author Dave Draper
 */
define(["alfresco/forms/controls/BaseFormControl",
        "alfresco/forms/controls/utilities/TextBoxValueChangeMixin",
        "dojo/_base/declare",
        "dojo/_base/lang",
        "dojo/dom-class",
        "dijit/form/NumberSpinner",
        "dojo/number",
        "alfresco/core/ObjectTypeUtils"], 
        function(BaseFormControl, TextBoxValueChangeMixin, declare, lang, domClass, NumberSpinner, number, ObjectTypeUtils) {
   
    /**
    * This extension of NumberSpinner is to "fix" certain inconsistencies
    * when working with non-integer numbers and different locales.
    *
    * @instance
    * @since 1.0.54
    */
   var DijitNumberSpinner = declare([NumberSpinner], {

      /**
       * Override the standard format method to avoid rounding decimals.
       *
       * @instance
       * @override
       * @param {number} value The number to format
       * @param {object} [constraints] The constraints
       * @returns {string} The formatted number
       */
      format: function alfresco_forms_controls_NumberSpinner__DijitNumberSpinner__format(value, constraints) {
         constraints = lang.mixin({}, constraints, {
            places: undefined
         });
         return this.inherited(arguments);
      },

      /**
       * Override the standard value-getter to always return a number if one is provided.
       *
       * @instance
       * @override
       * @returns {number} The value, as a number, or null if NaN
       */
      _getValueAttr: function alfresco_forms_controls_NumberSpinner__DijitNumberSpinner___getValueAttr() {
         var rawValue = this.textbox.value,
             numberValue = number.parse(rawValue),
             value = null;
         if (!isNaN(numberValue)) {
            value = numberValue;
            if (value === Math.round(value)) {
               value = Math.round(value);
            }
         }
         return value;
      },

      /**
       * Override the standard value-setter to correctly locale-format the content
       *
       * @instance
       * @override
       * @param {number} newValue The new value to set
       */
      _setValueAttr: function alfresco_forms_controls_NumberSpinner__DijitNumberSpinner___setValueAttr(newValue) {
         if (newValue !== null && !isNaN(newValue)) 
         {
            if (typeof newValue === "string") 
            {
               newValue = parseFloat(newValue);
            }
            this.textbox.value = number.format(newValue);
         }
      }
   });
   
   return declare([BaseFormControl, TextBoxValueChangeMixin], {
      
      /**
       * An array of the CSS files to use with this widget.
       * 
       * @instance
       * @type {object[]}
       * @default [{cssFile:"./css/NumberSpinner.css"}]
       */
      cssRequirements: [{cssFile:"./css/NumberSpinner.css"}],
      
      /**
       * This is the amount the value will be changed when using the "spin" controls
       * 
       * @instance
       * @type {number}
       * @default
       */
      delta: 1,

      /**
       * This is the minimum allowed number
       *
       * @instance
       * @type {number}
       * @default
       */
      min: null,

      /**
       * This is the maximum allowed number
       *
       * @instance
       * @type {number}
       * @default
       */
      max: null,

      /**
       * By default, this control can only be valid when it contains a numerical value. If this configuration
       * property is set to true, then it will be possible to submit a form without any value in this control,
       * at which point its value will be submitted as null.
       *
       * @instance
       * @type {boolean}
       * @default
       */
      permitEmpty: false,

      /**
       * How many decimal places are permitted in a valid value for this control
       *
       * @type {number}
       * @default
       */
      permittedDecimalPlaces: 0,

      /**
       * Returns the configuration for the widget ensuring that it is valid, in that
       * [min]{@link module:alfresco/forms/controls/NumberSpinner#min} and 
       * [max]{@link module:alfresco/forms/controls/NumberSpinner#max} but both be numerical values and
       * that [min]{@link module:alfresco/forms/controls/NumberSpinner#min} cannot be greater
       * than [max]{@link module:alfresco/forms/controls/NumberSpinner#max}.
       * 
       * @instance
       */
      getWidgetConfig: function alfresco_forms_controls_NumberSpinner__getWidgetConfig() {
         if (isNaN(this.min))
         {
            this.min = null;
         }
         if (isNaN(this.max))
         {
            this.max = null;
         }
         if ((this.min || this.min === 0) && (this.max || this.max === 0) && (this.max < this.min))
         {
            this.alfLog("warn", "The minimum allowed value is larger than the maxium allowed value, so constraints will be ignored", this);
            this.max = null;
            this.min = null;
         }
         this.configureValidation();
         return {
            id : this.generateUuid(),
            intermediateChanges: true,
            name: this.name,
            smallDelta: this.delta,
            constraints: {
               min: this.min,
               max: this.max,
               places: this.permittedDecimalPlaces
            }
         };
      },

      /**
       * This validator checks that the value provided is a number
       *
       * @instance
       * @param {object} validationConfig The configuration for this validator
       */
      isNumberValidator: function alfresco_forms_controls_FormControlValidationMixin__isNumberValidator(validationConfig) {
         var isValid = false;
         try 
         {
            var textBoxValue = this.wrappedWidget.textbox.value;
            var parsed = number.parse(textBoxValue);
            isValid = !isNaN(parsed) || (this.permitEmpty && textBoxValue === "");
         }
         catch (e) 
         {
            this.alfLog("warn", "Error validating number: ", e);
         }
         this.reportValidationResult(validationConfig, isValid);
      },

      /**
       * This validator checks that the value has no more than the specified number of decimal places (or isn't a number!)
       *
       * @instance
       * @param {object} validationConfig The configuration for this validator
       */
      decimalPlacesValidator: function alfresco_forms_controls_FormControlValidationMixin__decimalPlacesValidator(validationConfig) {
         var isValid = true;
         try 
         {
            // Get the raw value of the number box (for locales that use the "," for a decimal place, then the value will include
            // the ",")...
            var textBoxValue = this.wrappedWidget.textbox.value;

            // Parse the number (this will convert the "," to ".")...
            var parsed = number.parse(textBoxValue);
            if (isNaN(parsed))
            {
               // Can't do anything with a value that isn't a number - it should have been picked up by the "isNumberValidator" anyway
            }
            else
            {
               // Convert to a string... regardless of locale, the decimal place will be "." - we can use this to reliably work out
               // the number of decimal places specified...
               var string = parsed.toString();
               var decimalIndex = string.lastIndexOf(".");
               if (decimalIndex === -1)
               {
                  // There is no decimal place, nothing to validate...
               }
               else
               {
                  // Workout the decimal places...
                  var decimalPlaces = string.length - decimalIndex - 1; // Deduct additional one for index/length conversion
                  isValid = decimalPlaces <= this.permittedDecimalPlaces;
               }
            }
         } 
         catch (e) 
         {
            this.alfLog("warn", "Error validating number of decimal places: ", e);
         }
         this.reportValidationResult(validationConfig, isValid);
      },

      /**
       * This function is used to set or update the validationConfig as required based on the
       * [min]{@link module:alfresco/forms/controls/NumberSpinner#min} and 
       * [max]{@link module:alfresco/forms/controls/NumberSpinner#max} configuration.
       * 
       * @instance
       */
      configureValidation: function alfresco_forms_controls_NumberSpinner__configureValidation() {
         /*jshint maxcomplexity:false*/

         // Initialise validationConfig
         if (!this.validationConfig || ObjectTypeUtils.isObject(this.validationConfig))
         {
            this.validationConfig = [];
         }

         // Configure isNumber validation
         this.validationConfig.push({
            validation: "isNumberValidator",
            errorMessage: this.message("formValidation.numerical.error")
         });

         // Setup decimal places validation
         var decimalPlacesError = "formValidation.numerical.decimalPlaces0.error";
         if(this.permittedDecimalPlaces === 1) {
            decimalPlacesError = "formValidation.numerical.decimalPlaces1.error";
         } else if(this.permittedDecimalPlaces > 1) {
            decimalPlacesError = "formValidation.numerical.decimalPlacesN.error";
         }
         this.validationConfig.push({
            validation: "decimalPlacesValidator",
            errorMessage: this.message(decimalPlacesError, {
               0: this.permittedDecimalPlaces
            })
         });
         
         // Handle min/max validation
         if (this.min || this.min === 0 || this.max || this.max === 0)
         {
            var errorMessage;
            if (!this.max && this.max !== 0)
            {
               errorMessage = this.message("formValidation.numericalRange.min.error", { 0: this.min });
            }
            else if (!this.min && this.min !== 0)
            {
               errorMessage = this.message("formValidation.numericalRange.max.error", { 0: this.max });
            }
            else
            {
               errorMessage = this.message("formValidation.numericalRange.error", { 0: this.min, 1: this.max });
            }
            this.validationConfig.push({
               validation: "numericalRange",
               min: this.min,
               max: this.max,
               permitEmpty: this.permitEmpty,
               errorMessage: errorMessage
            });
         }
      },

      /**
       * Creates a new instance of a dijit/form/NumberSpinner as the wrapped form control.
       * 
       * @instance
       */
      createFormControl: function alfresco_forms_controls_NumberSpinner__createFormControl(config, /*jshint unused:false*/ domNode) {
         var additionalCssClasses = "";
         if (this.additionalCssClasses !== null)
         {
            additionalCssClasses = this.additionalCssClasses;
         }
         domClass.add(this.domNode, "alfresco-forms-controls-NumberSpinner " + additionalCssClasses);
         
         var ns = new DijitNumberSpinner(config);
         // We'll take care of the validation thanks very much!
         ns.isValid = function() {
            return true;
         };
         ns.onChange = lang.hitch(this, this.validate);
         return ns;
      },

      /**
       * Extends the [inherited function]{@link module:alfresco/forms/controls/BaseFormControl#setValue} to ensure
       * that only valid values can be set. Values must be numbers and must not be less than the minimum allowed
       * value nor greater than the maximum allowed value.
       *
       * @instance
       * @param {object} value The value to set.
       */
      setValue: function alfresco_forms_controls_NumberSpinner__setValue(value) {
         if (isNaN(value))
         {
            value = 0;
         }
         if ((this.min || this.min === 0) && value < this.min)
         {
            value = this.min;
         }
         if ((this.max || this.max === 0) && value > this.max)
         {
            value = this.max;
         }
         this.inherited(arguments);
      },

      /**
       * Remove commas and spaces from a string value, ready for number parsing<br />
       * <br />
       * NOTE: See AKU-341 for details of why we handle commas and spaces
       *
       * @instance
       * @param {string} value The value to parse
       * @returns {string} The cleaned value
       * @deprecated Since 1.0.54 - No longer required - extending modules should prepare for the removal of this function
       */
      _removeCommasAndSpaces: function alfresco_forms_controls_NumberSpinner___removeCommasAndSpaces(value) {
         return value && value.replace(/,|\s/g, "");
      },

      /**
       * Determine whether the supplied value string is a number.
       *
       * @instance
       * @param {string} value The value to check (should already be trimmed)
       * @returns {boolean} true if the value is a number
       * @deprecated Since 1.0.54 - No longer required - extending modules should prepare for the removal of this function
       */
      _valueIsNumber: function alfresco_forms_controls_NumberSpinner___valueIsNumber(value) {
         var isValidNumber = /^-?\d+(\.\d+)?$/.test(value);
         var parsedValue = parseFloat(value);
         return isValidNumber && !isNaN(parsedValue);
      }
   });
});