Source: src/main/javascript/oracle/oj/ojdvt-base/ojdvt-base.js

Oracle® JavaScript Extension Toolkit (JET)
1.1.2

E65298-01

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

/**
 * @ojcomponent oj.dvtBaseComponent
 * @augments oj.baseComponent
 * @since 0.7
 * @abstract
 */
oj.__registerWidget('oj.dvtBaseComponent', $['oj']['baseComponent'], {  
  //** @inheritdoc */ 
  _ComponentCreate : function() {
    this._super();
    
    // Create a reference div within the element to be used for computing relative event coords.
    this._referenceDiv = $(document.createElement("div"));
    this._referenceDiv.attr("style", "visibility:hidden;");
    this.element.append(this._referenceDiv);
  
    // Create the DvtContext, which creates the svg element and adds it to the DOM.
    var parentElement = this.element[0].parentElement;
    if (parentElement && parentElement._dvtcontext)
      this._context = parentElement._dvtcontext;
    else
      this._context = new dvt.DvtContext(this.element[0], null, this._referenceDiv[0]);
    
    // Set the reading direction on the context
    this._context.setReadingDirection(this._GetReadingDirection());
    
    // Set the tooltip and datatip callbacks and div style classes
    this._context.setTooltipAttachedCallback(oj.Components.subtreeAttached);
    this._context.setTooltipStyleClass('oj-dvt-tooltip');
    this._context.setDatatipStyleClass('oj-dvt-datatip');
    
    // Set high contrast mode if needed
    if ($(document.body).hasClass('oj-hicontrast'))
      dvt.DvtAgent.setHighContrast(true);
    
    // Create and cache the component instance
    this._component = this._CreateDvtComponent(this._context, this._HandleEvent, this);

    // Add the component to the display tree of the rendering context.
    this._context.getStage().addChild(this._component);  
    
    // Set the helpers for locale support
    this._setLocaleHelpers();
    
    // Retrieve and apply the translated strings onto the component bundle
    this._processTranslations();

    // Load component resources
    this._LoadResources();
  },
  
  //** @inheritdoc */      
  _AfterCreate : function () 
  {
    // Allow superclass to process root attributes and context menus
    this._super();
    
    this.element.attr("tabIndex", 0);
    
    // Render the component
    this._Render();
    
    // Add resize listener with 250ms delay
    this._resizeListener = this._handleResize.bind(this);
    oj.DomUtils.addResizeListener(this.element[0], this._resizeListener, 250);
  },
  
  //** @inheritdoc */ 
  refresh : function() {
    this._super();
  
    // Update the reading direction on the context
    this._context.setReadingDirection(this._GetReadingDirection());
    
    // Retrieve and apply the translated strings onto the component bundle
    this._processTranslations();
  
    // Render the component with any changes
    this._Render();
  },
  
  //** @inheritdoc */ 
  getNodeBySubId : function(locator) {
    var automation = (this._component && this._component.getAutomation) ? this._component.getAutomation() : null;
    if (automation) {
      // Convert the locator to the subid string, since the shared JS layer only accepts the subid string syntax.
      var subId = this._ConvertLocatorToSubId(locator);
      return automation.getDomElementForSubId(subId);
    }
  },
  
  //** @inheritdoc */ 
  getSubIdByNode : function(node) {
    var automation = (this._component && this._component.getAutomation) ? this._component.getAutomation() : null;
    if (automation) {
      // Retrieve the subid string from the shared JS layer, and convert it to the locator object.
      var subId = automation.getSubIdForDomElement(node);
      return subId ? this._ConvertSubIdToLocator(subId) : null;
    }
  },
  
  /**
   * Converts the specified locator object into a subId string.
   * @param {Object} locator
   * @return {string|null}
   * @protected
   * @instance
   * @memberof oj.dvtBaseComponent
   */
  _ConvertLocatorToSubId : function(locator) {
    // subclasses must override to support getNodeBySubId
    return null;
  },
  
  /**
   * Converts the specified subId string into a locator object.
   * @param {string} subId
   * @return {Object|null}
   * @protected
   * @instance
   * @memberof oj.dvtBaseComponent
   */
  _ConvertSubIdToLocator : function(subId) {
    // subclasses must override to support getSubIdByNode
    return null;
  },
  
  /**
   * Create dummy divs for style classes and merge style class values with json .
   * options object
   * @protected
   * @instance
   * @memberof oj.dvtBaseComponent
   */
  _ProcessStyles : function() {
    // Append the component style classes to the element
    var componentStyles = this._GetComponentStyleClasses();
    for(var i=0; i<componentStyles.length; i++) {
      this.element.addClass(componentStyles[i]);
    }
  	
  	// Process selectors for this component
    DvtStyleProcessor.processStyles(this.element, this.options, 
                                    this._GetComponentStyleClasses(), 
                                    this._GetChildStyleClasses());
  },
  
  /**
   * Returns the style classes associated with the component.
   * @return {Array}
   * @protected
   * @instance
   * @memberof oj.dvtBaseComponent
   */
  _GetComponentStyleClasses : function() {
    return ['oj-dvtbase'];
  },
  
  /**
   * Returns a map of the style classes associated with a component's children.
   * @return {Object}
   * @protected
   * @instance
   * @memberof oj.dvtBaseComponent
   */
  _GetChildStyleClasses : function() {
    return {};
  },
  
  /**
   * Returns an array of supported event types.  Used in conjunction with _setOptions to skip unnecessary rendering when 
   * event listeners are bound. Subclasses must override to return supported event types.
   * @return {Array}
   * @protected
   * @instance
   * @memberof oj.dvtBaseComponent
   */
  _GetEventTypes : function() {
    return [];
  },
  
  /**
   * Returns a map containing keys corresponding to the string ids in ojtranslations.js and values corresponding to the 
   * toolkit constants for the DvtBundle objects.  This map must be guaranteed to be a new instance so that subclasses can
   * add their translations to it.
   * @return {Object}
   * @protected
   * @instance
   * @memberof oj.dvtBaseComponent
   */
  _GetTranslationMap: function() {
    // The translations are stored on the options object.
    var translations = this.options['translations'];
  
    // Create the mapping to return.
    var ret = {
      'DvtUtilBundle.CLEAR_SELECTION': translations['labelClearSelection'],
      'DvtUtilBundle.COLON_SEP_LIST' : translations['labelAndValue'],
      'DvtUtilBundle.INVALID_DATA': translations['labelInvalidData'],
      'DvtUtilBundle.NO_DATA': translations['labelNoData'],
      
      // Accessibility
      'DvtUtilBundle.DATA_VISUALIZATION': translations['labelDataVisualization'],
      'DvtUtilBundle.STATE_SELECTED': translations['stateSelected'],
      'DvtUtilBundle.STATE_UNSELECTED': translations['stateUnselected'],
      'DvtUtilBundle.STATE_MAXIMIZED': translations['stateMaximized'],
      'DvtUtilBundle.STATE_MINIMIZED': translations['stateMinimized'],
      'DvtUtilBundle.STATE_EXPANDED': translations['stateExpanded'],
      'DvtUtilBundle.STATE_COLLAPSED': translations['stateCollapsed'],
      'DvtUtilBundle.STATE_ISOLATED': translations['stateIsolated'],
      'DvtUtilBundle.STATE_HIDDEN': translations['stateHidden'],
      'DvtUtilBundle.STATE_VISIBLE': translations['stateVisible'],
      
      'DvtUtilBundle.SCALING_SUFFIX_THOUSAND': translations['labelScalingSuffixThousand'],
      'DvtUtilBundle.SCALING_SUFFIX_MILLION': translations['labelScalingSuffixMillion'],
      'DvtUtilBundle.SCALING_SUFFIX_BILLION': translations['labelScalingSuffixBillion'],
      'DvtUtilBundle.SCALING_SUFFIX_TRILLION': translations['labelScalingSuffixTrillion'],
      'DvtUtilBundle.SCALING_SUFFIX_QUADRILLION': translations['labelScalingSuffixQuadrillion']
    };
    
    // Add abbreviated month strings
    var monthNames = oj.LocaleData.getMonthNames("abbreviated");
    ret['DvtUtilBundle.MONTH_SHORT_JANUARY'] = monthNames[0];
    ret['DvtUtilBundle.MONTH_SHORT_FEBRUARY'] = monthNames[1];
    ret['DvtUtilBundle.MONTH_SHORT_MARCH'] = monthNames[2];
    ret['DvtUtilBundle.MONTH_SHORT_APRIL'] = monthNames[3];
    ret['DvtUtilBundle.MONTH_SHORT_MAY'] = monthNames[4];
    ret['DvtUtilBundle.MONTH_SHORT_JUNE'] = monthNames[5];
    ret['DvtUtilBundle.MONTH_SHORT_JULY'] = monthNames[6];
    ret['DvtUtilBundle.MONTH_SHORT_AUGUST'] = monthNames[7];
    ret['DvtUtilBundle.MONTH_SHORT_SEPTEMBER'] = monthNames[8];
    ret['DvtUtilBundle.MONTH_SHORT_OCTOBER'] = monthNames[9];
    ret['DvtUtilBundle.MONTH_SHORT_NOVEMBER'] = monthNames[10];
    ret['DvtUtilBundle.MONTH_SHORT_DECEMBER'] = monthNames[11];
    
    return ret;
  },
  
  /**
   * Retrieves the translated resource with the specified 
   * @param {string} key The key used to retrieve the translated resource.
   * @param {Array.<string>} params The array of named parameters that need to be converted into index based parameters.
   * @protected
   * @instance
   * @memberof oj.dvtBaseComponent
   */
  _GetTranslatedResource: function(key, params) {
    var translatedResource = this.options['translations'][key];
    
    // If named parameters are defined, replace with index based params     
    if(params) {
      var paramMap = {};
      
      for(var i=0; i<params.length; i++) {
        paramMap[params[i]] = '{' + i + '}';
      }
      
      translatedResource = oj.Translations.applyParameters(translatedResource, paramMap);
    }

    return translatedResource;
  },
   
  /**
   * Called to process the translated strings for this widget.  
   * @private
   */
  _processTranslations: function() {
    // Retrieve the map of translation keys + DvtBundle identifiers
    var translationMap = this._GetTranslationMap();
    
    // Register with the DvtBundle
    dvt.DvtBundle.addLocalizedStrings(translationMap);
  },
  
  /**
   * @private
   * @instance
   * @memberof! oj.dvtBaseComponent
   */
  _setLocaleHelpers : function () {
    var helpers = {};
    
    // Number converter factory for use in formatting default strings
    helpers['numberConverterFactory'] = oj.Validation.getDefaultConverterFactory('number');
    
    // Date to iso converter to be called before passing to the date time converter
    helpers['dateToIsoConverter'] = function(input) {
      return (input instanceof Date) ? oj.IntlConverterUtils.dateToLocalIso(input) : input;
    }
    
    // Iso to date converter to be called for JS that requires Dates
    helpers['isoToDateConverter'] = function(input) {
      return (typeof(input) == 'string') ? oj.IntlConverterUtils.isoToLocalDate(input) : input;
    }
    
    this._context.setLocaleHelpers(helpers);
  },
  
  //** @inheritdoc */ 
  _destroy : function() {
    // Hide all component tooltips
    this._context.hideTooltips();
    
    // Call destroy on the JS component
    if (this._component.destroy)
      this._component.destroy();
      
    // Remove DOM resize listener  
    oj.DomUtils.removeResizeListener(this.element[0], this._resizeListener);
    
    // Remove children and clean up DOM changes
    this.element.children().remove();
    this.element.removeAttr('role').removeAttr('tabIndex');
    
    // Remove style classes that were added
    var componentStyles = this._GetComponentStyleClasses();
    for(var i=0; i<componentStyles.length; i++) {
      this.element.removeClass(componentStyles[i]);
    }
    
    // Call super last for destroy
    this._super();
  },

  //** @inheritdoc */ 
  _setOptions : function(options, flags) {
    // Call the super to update the property values
    this._superApply(arguments);
    
    // Render the component with the updated options.
    if(this._bUserDrivenChange) {
      // Option change fired in response to user gesture. Already reflected in UI, so no render needed.
      return;
    }
    else {
      // Event listeners don't require rendering.  Iterate through options to check for non-event options.
      // Also no render is needed if the component has exposed a method to update the option without rerendering.
      var bRenderNeeded = false;
      var eventTypes = this._GetEventTypes();
      var optimizedOptions = ['highlightedCategories', 'selection', 'dataCursorPosition'];
      $.each(options, function(key, value) {
        if(eventTypes.indexOf(key) < 0 && optimizedOptions.indexOf(key) < 0) {
          bRenderNeeded = true;
          return false;
        }
      }); 
        
      if(bRenderNeeded) 
        this._Render();
      else {
        // Update options without rerendering. Check for undefined to allow nulls.
        if (options['highlightedCategories'] !== undefined)
          this._component.highlight(options['highlightedCategories']);
        if (options['selection'] !== undefined)
          this._component.select(options['selection']);
        if (options['dataCursorPosition'] !== undefined && this._component.positionDataCursor)
          this._component.positionDataCursor(options['dataCursorPosition']);
      }
    }
  },
  
  
  /**
   * Called by _create to instantiate the specific DVT component instance.  Subclasses must override.
   * @param {dvt.DvtContext} context
   * @param {Function} callback
   * @param {Object} callbackObj
   * @protected
   * @instance
   * @memberof oj.dvtBaseComponent
   */
  _CreateDvtComponent : function(context, callback, callbackObj) {
    return null; // subclasses must override
  },
  
  /**
   * Called by the component to process events.  Subclasses should override to delegate DVT component events to their 
   * JQuery listeners.
   * @param {Object} event
   * @protected
   * @instance
   * @memberof oj.dvtBaseComponent
   */
  _HandleEvent : function(event) {
    // TODO: hiddenCategories and highlightedCategories should use DvtOptionChangeEvent
    
    var type = event && event.getType ? event.getType() : null;
    if (type === dvt.DvtCategoryHideShowEvent.TYPE_HIDE || type === dvt.DvtCategoryHideShowEvent.TYPE_SHOW) {
      this._UserOptionChange('hiddenCategories', event['hiddenCategories']);
    }
    else if (type === dvt.DvtCategoryRolloverEvent.TYPE_OVER || type === dvt.DvtCategoryRolloverEvent.TYPE_OUT) {
      this._UserOptionChange('highlightedCategories', event['categories']);
    }
    else if (type === dvt.DvtOptionChangeEvent.TYPE) {
      this._UserOptionChange(event['key'], event['value'], event['optionMetadata']);
    }
    else if (this.options['contextMenu'] && type === dvt.DvtComponentTouchEvent.TOUCH_HOVER_END_TYPE) {
      this._OpenContextMenu($.Event(event.getNativeEvent()), 'touch');
    }
  },
  
  /**
   * Called when the component is resized.
   * @param {number} width
   * @param {number} height
   * @private
   * @instance
   * @memberof oj.dvtBaseComponent
   */
  _handleResize : function(width, height) {
    // Render the component at the new size if it changed enough
    var newWidth = this.element.width();
    var newHeight = this.element.height();
    if(this._width == null || this._height == null || (Math.abs(newWidth - this._width) + Math.abs(newHeight - this._height) >= 5)) 
      this._Render(true);
  },
  
  /**
   * Called once during component creation to load resources.
   * @protected
   * @instance
   * @memberof oj.dvtBaseComponent
   */
  _LoadResources : function() {
    // subcomponents should override
  }, 
          
  /**
   * Called to render the component at the current size.
   * @param {boolean} isResize (optional) Whether it is a resize rerender.
   * @protected
   * @instance
   * @memberof oj.dvtBaseComponent
   */
  _Render : function(isResize) {
    // Fix 18498656: If the component is not attached to a visible subtree of the DOM, rendering will fail because 
    // getBBox calls will not return the correct values.  Log an error message in this case and avoid rendering.
    // Note: Checking offsetParent() does not work here since it returns false for position: fixed.
    if(!this._context.isReadyToRender()){
      oj.Logger.info(this.options['translations']['messageNotReadyToRender']['summary']);
      this._renderNeeded = true; 
    }
    else {
      // Add the width, height, and locale as private fields in the options for debugging purposes
      this._width = this.element.width();
      this._height = this.element.height();
      this.options['_width'] = this._width;
      this.options['_height'] = this._height;
      this.options['_locale'] = oj.Config.getLocale();
    
      // Merge css styles with with json options object
      this._ProcessStyles();
      
      // Render the component. Skip the options on resize to suppress animation.
      this._component.render(isResize ? null : this.options, this._width, this._height);
      
      this._renderNeeded = false;
    }
  },
  
  //** @inheritdoc */ 
  _NotifyShown: function()
  {
    this._super();
    if(this._renderNeeded)
      this._Render();
  },
  
  //** @inheritdoc */ 
  _NotifyAttached: function()
  {
    this._super();
    if(this._renderNeeded)
      this._Render();
  },
    
  //** @inheritdoc */ 
  _NotifyDetached : function() {
    this._super();
    this._context.hideTooltips();
  },
  
  //** @inheritdoc */ 
  _NotifyHidden : function() {
    this._super();
    this._context.hideTooltips();
  },
  
  /**
   * Sets an option change that was driven by user gesture.  Used in conjunction with _setOption to ensure that the
   * correct optionMetadata flag for writeback is set.
   * @param {string} key The name of the option to set.
   * @param {Object} value The value to set for the option.
   * @param {Object} optionMetadata The optionMetadata for the optionChange event
   * @memberof oj.dvtBaseComponent
   * @instance
   * @protected
   */
  _UserOptionChange : function(key, value, optionMetadata) {
    this._bUserDrivenChange = true;

    this.option(key, value, {'_context': {writeback:true, optionMetadata: optionMetadata, internalSet: true}});
    
    this._bUserDrivenChange = false;
  },
  
  //** @inheritdoc */ 
  _NotifyContextMenuGesture: function(menu, event, eventType) {
    // DVTs support context menus on touch hold release which is detected by the 
    // toolkit and handled in _HandleEvent after receiving touch hold release event.
    if (eventType === "touch")
      return;
    // Position context menus relative to the current keyboard focus when keyboard triggered
    else if (eventType === "keyboard")
      this._OpenContextMenu(event, eventType, {"position": {"of": this._component.getKeyboardFocus()}});
    else
      this._super(menu, event, eventType);
  },
  
  /**
   * Returns a DVT component associated with a DOMElement
   * @param {Element} element The DOMElement to get the DVT component from.
   * @return {Object} The DVT component associated with the DOMElement or null
   * @memberof oj.dvtBaseComponent
   * @instance
   * @protected
   */
  _GetDvtComponent: function(element) {
    var widget = oj.Components.getWidgetConstructor(element)("instance");
    if (widget) {
      return widget._component;
    }
    return null;
  },
  
  
  /**
   * Adds getters for the properties on the specified map.
   * @param {Object|null} map
   * @memberof oj.dvtBaseComponent
   * @instance
   * @protected
   */
  _AddAutomationGetters: function(map) {
    if(!map)
      return;
    
    // Bug 20884377: Adds legacy getters to the object maps returned by the automation code. These getters are 
    // deprecated and will be removed in 1.2.0.
    var props = {};
    for (var key in map) {
      this._addGetter(map, key, props);
    }
    Object.defineProperties(map, props);
  },
  
  /**
   * Adds getter for the specified property on the specified properties map.
   * @param {Object} map
   * @param {string} key
   * @param {Object} props The properties map onto which the getter will be added.
   * @memberof oj.dvtBaseComponent
   * @instance
   * @private
   */
  _addGetter: function(map, key, props) {
    var prefix = (key == 'selected') ? 'is' : 'get';
    var getterName = prefix + key.charAt(0).toUpperCase() + key.slice(1);
    props[getterName] = {'value': function() { return map[key] }};
  },
  
  /**
   * Converts an indexPath array to a string of the form '[index0][index1]...[indexN]'.
   * @param {Array} indexPath
   * @return {string} The resulting string.
   * @instance
   * @protected
   */
  _GetStringFromIndexPath : function(indexPath) {
    var ret = '';
    for(var i=0; i<indexPath.length; i++) {
      ret += '[' + indexPath[i] + ']';
    }
    return ret;
  },
  
  /**
   * Converts a string containing indices in the form '[index0][index1]...[indexN]' to an array of indices.
   * @param {string} subId
   * @return {Array} The resulting array to be used for locator indexPath.
   * @instance
   * @protected
   */
  _GetIndexPath : function(subId) {
    var indexPath = [];
    var currentIndex = 0;
    while(subId.indexOf('[', currentIndex) > 0) {
      var start = subId.indexOf('[', currentIndex) + 1;
      var end = subId.indexOf(']', currentIndex)
      indexPath.push(Number(subId.substring(start, end)));
      currentIndex = end + 1;
    }
    return indexPath;
  },
  
  /**
   * Converts a string containing a single index in the form '[index]' into the numerical index.
   * @param {string} subId
   * @return {number}
   * @instance
   * @protected
   */
  _GetFirstIndex : function(subId) {
    return Number(this._GetFirstBracketedString(subId));
  },
  
  /**
   * Returns the first bracketed substring in the specified string.
   * @param {string} subId
   * @return {string}
   * @instance
   * @protected
   */
  _GetFirstBracketedString : function(subId) {
    var start = subId.indexOf('[') + 1;
    var end = subId.indexOf(']');
    return subId.substring(start, end);
  }
  
}, true);