Source: src/main/javascript/oracle/oj/ojeditablevalue/EditableValueUtils.js

Oracle® JavaScript Extension Toolkit (JET)
1.1.2

E65298-01

/**
 * Copyright (c) 2014, Oracle and/or its affiliates.
 * All rights reserved.
 */

/**
 * @class JET Editable Component Utils
 * @export
 * @since 0.6
 * 
 */
oj.EditableValueUtils = {};

/**
 * This method is called during _InitOptions() to initialize a component option value from DOM. This 
 * uusally is the case when the option value is undefined, 
 * i.e., this.options.optionName === undefined.
 * <br/>
 * Returns the attribute value for the given attribute on the element appropriately converted, or 
 * the default, if the attribute isn't set on the element.<br/>
 * 
 * @param {Object} element the element the component is initialized with.
 * @param {string} attribute the name of the element's attribtue. Example, value, disabled etc.
 * 
 * @returns {Object} a JSON object containing the following properties - <br/>
 * <ul>
 * <li><b>fromDom</b> - whether the option value was initialized from DOM. When true the option's 
 * value is written back (to observable).</li> 
 * <li><b>value</b> - the option value. the attribute value or the default if the attribute isn't 
 * set on the element.</li> 
 * </ul>
 * 
 * @private
 */
oj.EditableValueUtils.getAttributeValue = function (element, attribute)
{
  var result, attrVal, propVal, returnVal = {}; 
  
  if (element && attribute)
  {
    switch (attribute)
    {
      case "disabled" :
        result = element.attr("disabled") !== undefined ? 
          !!element.prop("disabled") : undefined;
        break;
        
      case "pattern":
        result = element.prop("pattern") || undefined; 
        break; 
        
      case "placeholder":
        result = element.prop("placeholder") || undefined;
        break;
        
      case "readonly": 
        result = element.attr("readonly") !== undefined ? 
          !!element.prop("readonly") : undefined; 
        break;
      
      case "required":
        attrVal = element.attr("required");
        propVal = element.prop("required");

        // If attribute is present and not undefined
        //   - In IE9, required attribute is not supported at all, so attr() is defined, prop() is 
        //   undefined. In such cases set default to !!attrVal
        //   - Otherwise set to !!propVal
        // If neither attr is undefined then use defaultValue
        // TODO: review needed 
        result = (attrVal !== undefined) ? 
          ((propVal !== undefined) ? !!propVal : !!attrVal) : undefined;
        
        break;
      
      case "title":
        result = element.attr("title") !== undefined ? 
          element.prop("title") : undefined;
        break;
        
      case "value":
        // element attribute may not be set, in which case default to null
        result = element.val() || undefined;
        break;
      
      case "min": 
        //same logic for min + max as in default
      case "max":
      
      default: 
        result = element.attr(attribute) || undefined;
        break;
    }
  }
  
  if (result !== undefined)
  {
    returnVal.fromDom = true;
    returnVal.value = result;
  }
  else
  {
    returnVal.fromDom = false;
    // returnVal.value = defaultValue; 
  }
  return returnVal;
};

/**
 * Called from component._InitOptions(), when a component might need to initialize its options from 
 * DOM. This usually is done when the constructor option is undefined, even in the case where 
 * this.options.option has a value that comes from app defaults or component (original) default. 
 * See below for details.
 * <p>
 * IMPORTANT: Do not call this method after component has been created, since option values are 
 * mutated directly after that point.</p>
 * 
 * The 'final' value an option uses/initializes from, can come from these places (in order of least 
 * to most likely) - <br/>
 * <ol>
 * <li>component default - this is the widget default </li><br/>
 * <li>app default - this is what a page author defines for the value in the page/app</li> <br/>
 * <li>dom value - if your option also has a dom attribute, this is the value set on element for 
 * component. </li> <br/>
 * <li>constructor value - this is the value page author sets on the component binding </li><br/>
 * </ol>
 * 
 * At the time _InitOptions is called, (1), (2) and (4) are merged, but this may not be the value a 
 * component wants for the option, especially when (4) is undefined. For example, if these values 
 * were set for a component - <br/>
 * (1) - 'foo'<br/>
 * (2) - 'bar'<br/>
 * (3) - 'lucy'<br/>
 * (4) - undefined<br/>
 * <p>
 * at the time _InitOptions is called, this.options.option is set to 'bar', but because DOM value 
 * wins over app default or component default, component needs to check if the constructor value was 
 * undefined and if so, set option to 'lucy'. <br/>
 * If the dom value is not set, then the component provided default value - defaultOptionValue gets 
 * used. This method always defaults the value to be - this.options.option || defaultOptionValue -  
 * because we think if neither (3) nor (4) is set, then the value from (2) should win over the 
 * defaultOptionValue. <br/>
 * </p>
 * 
 * @param {Object} props an Object literal that a component provides with the following properties 
 * that helps determine the final value for one or more options.
 * 
 * @property {string} props.attribute - name of DOM attribute
 * @property {string|undefined} props.option - name of the option if different from attribute name.
 * @property {Object|null|string|number|boolean} props.defaultOptionValue - the default value to use
 * for the option when the DOM value is not set. For example, editable components bound to inputs, 
 * would pass false as the default for disabled option, while components like the *set components 
 * would pass null, because they support a tri-state.
 * 
 * @property {Function|boolean|undefined} props.coerceDomValue - if the DOM value is set and 
 * coercing the dom value is needed, then either set to boolean true, which uses the default 
 * coercion rules for common attributes (a), or provide a custom callback (b). <p>
 * E.g., 'value' option for input number, input date etc. have special rules for coercing the value,
 *  so thse provide a custom callback. For common attributes like required and disabled, set the 
 *  value to true so the default oj.EditableValueUtils#coerceDomValueForOption method gets used.
 *  
 * @property {boolean|undefined} props.validateOption - if set to true, then it calls 
 * oj.EditableValueUtils.validateValueForOption method to validate the option.
 * 
 * @param {Object} constructorOptions the options set on the component instance, often using 
 * component binding.
 * @param {Object} comp component instance.
 * @param {Function=} postprocessCallback - optional callback that will receive a map of initialized
 * options for post-processing
 *  
 * @public
 */
oj.EditableValueUtils.initializeOptionsFromDom = function (props, constructorOptions, comp, postprocessCallback)
{
  var initializedOptions = {};
  
  // Loop through props to initialize option 
  for (var i = 0; i < props.length; i++)
  {
    var finalValue, result, prop = props[i], 
        attribute = prop.attribute, 
        option = prop.option || attribute, 
        coerceDomValue = prop.coerceDomValue,
        validateOption = prop.validateOption,
        element = comp.element, 
        previousValue = comp.options[option];

    /* The precedence for the value that an option uses is as follows from lowest to highest -
     *
     * (1) component default - this is the widget default
     * (2) app default - this is what a page author defines for the value in the page / app
     * (3) dom value - if your option also has a dom attribute, this is the value set on element. 
     * (4) constructor value - this is the value page author sets on the component binding
     *      
     * When (4) is undefined then attempt to default from (3). But if (3) is undefined use a 
     * defaultValue that component determines. If constructor option was not set then default 
     * value is the merged option value (i.e., this.options) || a hard-coded default determined 
     * by component.
     */

    // Step 1: use DOM value
    if (constructorOptions[option] === undefined)
    {
      previousValue = comp.options[option];
      result = oj.EditableValueUtils.getAttributeValue(element, attribute);

      // if we are using domValue then coerce the dom value before writing to options and trigegr 
      // option change so the value is written back (to ko)
      if (result.fromDom)
      {
        finalValue = result.value;

        // only required needs coercing so not bad
        if (coerceDomValue) 
        {
          if (typeof coerceDomValue === "boolean")
          {
            finalValue = oj.EditableValueUtils.coerceDomValueForOption(option, finalValue);
          }
          else if (typeof coerceDomValue === "function")
          {
            finalValue = coerceDomValue.call(comp, finalValue);
          }
        }
        initializedOptions[option] = finalValue;
      }
      else
      {
        // hmm, no dom value found; if comp.options also happens to be undefined then simply use 
        // the default option value. 
        if (previousValue === undefined)
        {
          initializedOptions[option] = prop.defaultOptionValue;
        }
      }
    }
    
    var valueToValidate = (option in initializedOptions) ? initializedOptions[option] : previousValue;

    // Step 2: validate the option value if needed
    if (validateOption)
    {
      if (typeof validateOption === "boolean")
      {
        oj.EditableValueUtils.validateValueForOption(option, valueToValidate);
      }
    }
  }
  
  if (postprocessCallback != null)
  {
    postprocessCallback(initializedOptions);
  }
  
  comp.option(initializedOptions, {'_context': {writeback: true, internalSet: true}});
};

  /**
   * Validates value set for the option and throws error if invalid.
   * 
   * @param {string} option name of the option. Validates options common to all edtiableValue 
   * holders.
   * @param {string|Object|boolean|number|undefined} value of the option that is validated
   * 
   * @throws {Error} if option value is invalid
   * @public
   */
  oj.EditableValueUtils.validateValueForOption = function (option, value)
  {
    var error = false;
    
    switch (option)
    {
      case 'required' :
        if (value !== null &&  typeof value !== "boolean")
        {
          error = true; 
        }
        break;
      
      case "readOnly":
      case "disabled" :
        if (value !== null &&  typeof value !== "boolean")
        {
          error = true;
        }
        
        break;
    }
    
    if (error)
    {
      throw "Option '" + option + "' has invalid value set: " + value;
    }
  };
  
  
  /**
   * Coerces the dom value being used for the option, and throws error if invalid.
   * 
   * @param {string} option name of the option.
   * @param {string|Object|boolean|number|null} domValue dom value that is being coerced to the 
   * option value
   * @throws {Error} if domValue cannot be coerced appropriately
   * @public
   */
  oj.EditableValueUtils.coerceDomValueForOption = function(option, domValue) 
  { 
    var coerced = domValue;
    switch (option)
    {
      case 'required' :
        coerced = domValue ? true : false;
        break;
    }
    
    return coerced;
  };