Source: src/main/javascript/oracle/oj/ojswitch/ojswitch.js

Oracle® JavaScript Extension Toolkit (JET)
1.1.2

E65298-01

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

/**
 * @preserve Copyright 2013 jQuery Foundation and other contributors
 * Released under the MIT license.
 * http://jquery.org/license
 */

(function() {

/*!
 * JET Switch @VERSION
 */
/**
 * @ojcomponent oj.ojSwitch
 * @augments oj.editableValue
 * @classdesc
 * <p>
 * Component JET Switch (ojSwitch) enhances <code class="prettyprint">input</code> 
 * element and manages the selection of Boolean values.
 * </p>
 *
 * <h3 id="touch-section">
 *   Touch End User Information
 *   <a class="bookmarkable-link" title="Bookmarkable Link" href="#touch-section"></a>
 * </h3>
 *
 * {@ojinclude "name":"touchDoc"}
 *
 *
 * <h3 id="keyboard-section">
 *   Keyboard End User Information
 *   <a class="bookmarkable-link" title="Bookmarkable Link" href="#keyboard-section"></a>
 * </h3>
 *
 * {@ojinclude "name":"keyboardDoc"}
 *
 *
 * <h3 id="a11y-section">
 *   Accessibility
 *   <a class="bookmarkable-link" title="Bookmarkable Link" href="#a11y-section"></a>
 * </h3>
 * 
 * <p>
 * The component is accessible; it sets and maintains the appropriate aria- attributes, 
 * like aria-checked and aria-disabled.
 * </p>
 * <p>
 * It is up to the application developer to associate the label to the input component. 
 * For Switch, you should put an <code class="prettyprint">id</code> on the input, and 
 * then set the <code class="prettyprint">for</code> attribute on the label to be the input's id.
 * </p>
 *
 * <h3 id="label-section">
 *   Label and Switch
 *   <a class="bookmarkable-link" title="Bookmarkable Link" href="#label-section"></a>
 * </h3>
 * <p>
 * For accessibility, you should associate a label element with the input by putting an 
 * <code class="prettyprint">id</code> on the input, and then setting the 
 * <code class="prettyprint">for</code> attribute on the label to be the input's id.
 * </p>
 * <h3 id="state-section">
 *   Setting the Value Option
 *   <a class="bookmarkable-link" title="Bookmarkable Link" href="#state-section"></a>
 * </h3>
 * <p>The value option should be Boolean. If the value option is undefined, then it is <code class="prettyprint">false</code>.
 * </p>
 * <!-- - - - - Above this point, the tags are for the class.
 *              Below this point, the tags are for the constructor (initializer). - - - - - - -->
 * 
 * @desc Creates or re-initializes a JET Switch. 
 * 
 * @param {Object=} options a map of option-value pairs to set on the component
 * 
 * @example <caption>Initialize the switch with no options specified:</caption>
 * $(".selector").ojSwitch();
 * 
 * @example <caption>Initialize the switch with some options specified:</caption>
 * $(".selector").ojSwitch({"value": true, "disabled": false});
 */ 
oj.__registerWidget("oj.ojSwitch", $['oj']['editableValue'], {
	version: '1.1.0',
	defaultElement: '<input>',
	widgetEventPrefix: 'oj',

	options:
	{    
    /** 
     * <p>
     * Disabled <code class="prettyprint">true</code> disables the component and disables 
     * all the inputs/labels. 
     * Disabled <code class="prettyprint">false</code> enables the component, and leaves the inputs 
     * disabled state as it is in the DOM.
     * <p>
     * 
     * @example <caption>Initialize component with <code class="prettyprint">disabled</code> option:</caption>
     * $(".selector").ojSwitch({"disabled": true});
     * 
     * @expose 
     * @type {?boolean}
     * @default <code class="prettyprint">false</code>
     * @public
     * @instance
     * @memberof oj.ojSwitch
     */
	disabled: false,	

	/**
	 * @private
     * @instance
     * @memberof oj.ojSwitch
	 */
	readOnly: false,

	/**
	 * @private
     * @instance
     * @memberof oj.ojSwitch
	 */
	required: false,
	
    /** 
     * Represents advisory information for the component, such as would be appropriate for a tooltip. 
     * 
     * <p>
     * When a title is present it is by default displayed in the notewindow, or as determined by the 
     * 'title' property set on the <code class="prettyprint">displayOptions</code> option. 
     * When the <code class="prettyprint">title</code> option changes the component refreshes to 
     * display the new title. 
     * </p>
     * 
     * @example <caption>Initialize component with <code class="prettyprint">title</code> option:</caption>
     * $(".selector").ojSwitch({"title": "My Switch"});
     * 
     * @expose 
     * @type {string|undefined}
     * @default when the option is not set, the element's title attribute is used as its initial 
     * value if it exists.
     * @public
     * @instance
     * @memberof oj.ojSwitch
     */
	title: undefined,
	
	/** 
     * The state of the switch component (true/false).
     * 
     * @example <caption>Initialize component (switch is ON) with <code class="prettyprint">value</code> option:</caption>
     * $(".selector").ojSwitch({"value": true});
     * 
     * @expose 
     * @type {?boolean}
     * @default <code class="prettyprint">false</code>
     * @public
     * @instance
     * @memberof oj.ojSwitch
     */
	value: false
	},

	// P U B L I C    M E T H O D S

	/**
 	 * Refreshes the switch
	 * <p>A <code class="prettyprint">refresh()</code> or re-init is required 
	 * when a switch is programatically changed.
	 *
	 * @example <caption>Invoke the <code class="prettyprint">refresh</code> method:</caption>
	 * $( ".selector" ).ojSwitch( "refresh" );
	 * @expose 
	 * @memberof oj.ojSwitch
	 * @instance
	 */
	refresh: function() 
	{
		this._super();   
		this._setup();
	},

	/**
	 * Returns a jQuery object containing the element visually representing the switch. 
	 * 
	 * <p>This method does not accept any arguments.
	 * 
	 * @expose
	 * @memberof oj.ojSwitch
	 * @instance
	 * @return {jQuery} the switch
	 */	
	widget : function ()
	{
		return this._element2;
	},

	/**
	 * Return the subcomponent node represented by the documented locator attribute values.
	 * <p>
	 * If the locator is null or no subId string is provided then this method returns the element that this component was initalized with.
	 * </p>
	 * <p>
	 * If a subId was provided but a subcomponent node cannot be located this method returns null.
	 * </p>
	 * <p> 
	 * Test authors should target switch sub elements using the following names:
	 * </p>
	 * <ul>
	 * <li><b>oj-switch-track</b>: span represents dom for switch track</li>
	 * <li><b>oj-switch-thumb</b>: span represents dom for switch thumb</li>
	 * </ul>
	 * 
	 * @expose
	 * @override
	 * @memberof oj.ojSwitch
	 * @instance
	 * @param {Object} locator An Object containing at minimum a subId property 
	 * whose value is a string, documented by the component, that allows the component to 
	 * look up the subcomponent associated with that string.  It contains:
	 * <ul>
	 * <li>
	 *   component: optional - in the future there may be more than one component 
	 *   contained within a page element
	 * </li>
	 * <li>
	 *   subId: the string, documented by the component, that the component expects 
	 *   in getNodeBySubId to locate a particular subcomponent 
	 * </li>
	 * </ul>  
	 * @returns {Element|null} the subcomponent located by the subId string 
	 * passed in locator, if found.
	 */	
	getNodeBySubId: function(locator)
	{
		var node, subId, rootElement = this.widget();
		
		node = (locator == null || locator['subId'] == null) ? rootElement : null;
		if (!node)
		{
			subId = locator['subId'];
			if (subId === 'oj-switch-thumb' ||
				subId === 'oj-switch-track') 
			{
				return rootElement.find('.'+subId)[0];
			}
			
		}
		return node || null;
	},

	// P R O T E C T E D    C O N S T A N T S   A N D   M E T H O D S

	/**
	 * Overridden to set the options.value. When constructorOptions value is undefined,
	 * we read the CHECKED, DISABLED, READONLY, REQUIRED options on the checkbox and 
	 * build the switch options from that.
	 * 
	 * @memberof oj.ojSwitch
	 * @instance
	 * @protected
	 */  
	_InitOptions : function (originalDefaults, constructorOptions)
	{
		var props, val;
		
		props = [
			{attribute: 'disabled',                     defaultOptionValue: false, validateOption: true},
			{attribute: 'readonly', option: 'readOnly', defaultOptionValue: false, validateOption: false, coerceDomValue: function() {return false;}},
			{attribute: 'required',                     defaultOptionValue: false, validateOption: false, coerceDomValue: function() {return false;}},
			{attribute: 'checked',  option: 'value',    defaultOptionValue: false, validateOption: false, coerceDomValue: function(domValue) {return domValue ? true : false;}},
			{attribute: 'title',                        defaultOptionValue: undefined}
		];
				
		this._super(originalDefaults, constructorOptions);
		oj.EditableValueUtils.initializeOptionsFromDom(props, constructorOptions, this);

		val = this.option('value');
		this.option({readOnly: false, required: false, value: !!val}, {'_context':{writeback: true, internalSet: true}});
		
	},

	/**
	 * Create switch component
	 * 
	 * @override
	 * @protected
	 * @memberof oj.ojSwitch
	 * @instance
	 */
	_ComponentCreate: function()
	{
		this._super();
		
		if(!this.element.is('input'))
		{
			throw new Error('ojSwitch can be bound to INPUT only.');
		}
		
		this._inputElementOriginalDisplay = this.element.css("display");
		this.element
			.css("display", "none")
			.attr('type', 'checkbox')
			.attr('checked', this.option('value'))
			.attr( 'tabindex', '-1' )
			.attr('disabled', this.option('disabled'))
			.attr('readonly', this.option('readOnly'));
			
		this._element2 = this.element
			.wrap('<div></div>') // @HTMLUpdateOK trusted string
			.parent()
				.addClass( 'oj-switch oj-component oj-form-control' );
		this._element2
			.append("<div class='oj-switch-track'><div class='oj-switch-thumb' tabIndex='0'></div></div>"); // @HTMLUpdateOK append or prepend trusted new DOM to switch elem
		
		this.switchThumb = this._element2.find('.oj-switch-thumb');	
		this.switchThumb
			.attr( 'role', this._setSwitchRole() );

		// Set aria-label.. : aria-labelledby or aria-label
		this._setAriaLabel(this.element, this.switchThumb);
		this._setupEvents();
		this._setup();
		
	},

	/**
	 * Set ARIA label for the switch: aria-labelledby or aria-label
	 * 
	 * @protected
	 * @memberof oj.ojSwitch
	 * @instance
	 */		
	_setAriaLabel: function(elem, target)
	{
		var id = elem.prop('id'),
			ariaLabelledBy = elem.attr('aria-labelledby'),
			ariaLabel = elem.attr('aria-label'),
			switchLabel;
			
		if(ariaLabel) // 1. <input aria-label='...' is set
		{
			target.attr('aria-label', ariaLabel);
		} 
		else if(ariaLabelledBy) // 2. <input aria-labelledby='...' is set
		{
			target.attr('aria-labelledby', ariaLabelledBy);
		}
		else
		{
			if(id)
			{
				switchLabel = $("label[for='"+id+"']");
				if(switchLabel) // 3. <label for='...'
				{
					if(switchLabel.attr('id'))
					{
						target.attr('aria-labelledby', switchLabel.attr('id'));
					}
					else
					{
						target.attr('aria-label', $(switchLabel).text());
					}
				}
			}
		}		
	},
	
	/**
	 * _setup is called on create and refresh. 
	 *
	 * @memberof oj.ojSwitch
	 * @instance
	 * @private
	 */
	_setup: function()
	{
		var rootElement = $(this.widget()),
			thumbTitle = this.option('title');
	
		if(rootElement === undefined) return;
		this.element
			.attr('checked', this.option('value')); // Switch vs Input synchonization
		
		rootElement.removeClass('oj-disabled oj-selected oj-hover oj-active');
		$(this.switchThumb).attr('tabindex', '0');
		
		if(this.option('disabled'))
		{
			rootElement.addClass('oj-disabled');
			$(this.switchThumb).removeAttr('tabindex');
		}

		if( this.option('value') )
		{
			rootElement.addClass('oj-selected');
		}
		
		// Title
		if(thumbTitle !== undefined)
		{
			$(this.switchThumb).attr('title', thumbTitle);
		}
		
		// Aria
		$(this.switchThumb).attr( 'aria-checked', this.option('value') );
		
		$(this.switchThumb).removeAttr( 'aria-disabled' );
		rootElement.removeAttr( 'aria-disabled' );
		if(!this._CanSetValue())
		{
			$(this.switchThumb).attr( 'aria-disabled', 'true');
		}
	},

	/**
	 * Binding events to the switch component
	 * 
	 * @protected
	 * @memberof oj.ojSwitch
	 * @instance
	 */
	_setupEvents: function()
	{
	    if(this._CanSetValue()) 
		{
			this._on( this._element2, this._switchEvents );
		}
	    this._hoverable( this._element2 );
	    this._focusable( this._element2 );
	},
	
	/**
	 * @override
	 * @private
	 */
	_switchEvents: 
	{
		keydown: function( event )
		{
			// ENTER and SPACE will change the switch
			if ( event.which === $.ui.keyCode.ENTER || event.which === $.ui.keyCode.SPACE)
			{
				$( event.currentTarget ).addClass( "oj-active" );
				event.preventDefault();
			}
		},
		keyup: function( event )
		{
			// ENTER and SPACE will change the switch
			if ( event.which === $.ui.keyCode.ENTER || event.which === $.ui.keyCode.SPACE)
			{
				this._SetValue(!this.option('value'), event);
			}
		},		
		mousedown: function( event )
		{
			
			// LEFT MOUSE BUTTON will change the switch
			if (event.which === 1)
			{
				$( event.currentTarget ).addClass( "oj-active" );
			}
		},
		mouseup: function( event )
		{
			// LEFT MOUSE BUTTON will change the switch
			if (event.which === 1)
			{
				this._SetValue(!this.option('value'), event);
			}
		},
		touchstart: function( event )
		{
			this._SetValue(!this.option('value'), event);
			event.preventDefault();
		}		
	},

	/**
	 * Returns the default styleclass for the component. Currently this is 
	 * used to pass to the _ojLabel component, which will append -label and 
	 * add the style class onto the label. This way we can style the label
	 * specific to the input component. For example, for inline labels, the
	 * checkboxset/checkboxset components need to have margin-top:0, whereas all the
	 * other inputs need it to be .5em. So we'll have a special margin-top style 
	 * for .oj-label-inline.oj-checkboxset-label
	 * All input components must override
	 * 
	 * @return {string}
	 * @memberof oj.ojSwitch
	 * @override
	 * @protected
	 */
	_GetDefaultStyleClass: function()
	{
		return 'oj-switch';
	},

	/**
	 * Returns switch role for ARIA
	 * ToDo: for IE it should be role="cehckbox"
	 * 
	 * @return {string}
	 * @memberof oj.ojSwitch
	 * @override
	 * @protected
	 */
	_setSwitchRole: function()
	{
		return 'switch checkbox';
	},

	/**
	 * @ignore
	 * @protected
	 * @override
	 */
	_destroy: function()
	{
		this._element2.find('.oj-switch-track').remove();	
		oj.DomUtils.unwrap(this.element);
		this._RestoreAttributes(this.element);		
		return this._super();;
	},

	/**
	 * Returns a jquery object of the elements representing the content nodes (switch).
	 * @protected
	 * @override
	 * @memberof oj.ojSwitch
	 */
	_GetContentElement : function ()
	{
		return this._element2;
	},

	/**
	 * @override
	 * @private
	 */
	_setOption: function( key, value, flags )
	{				
		var coercedValue;
		switch(key)
		{
			case 'disabled':
			case 'value':
				coercedValue = !!value;
				break;
			case 'readOnly':
			case 'required':
				coercedValue = false;
				break;
			default:
				coercedValue = value;
		}				
		this._super( key, coercedValue, flags );			
		this._setup();	
	}

    // Fragments:
    
    /**
     * <table class="keyboard-table">
     *   <thead>
     *     <tr>
     *       <th>Target</th>
     *       <th>Gesture</th>
     *       <th>Action</th>
     *     </tr>
     *   </thead>
     *   <tbody>
     *     <tr>
     *       <td>Switch Thumb</td>
     *       <td><kbd>Tap</kbd></td>
     *       <td>Toggle switch value</td>
     *     </tr>
     *   </tbody>
     * </table>
     *
     * @ojfragment touchDoc - Used in touch gesture section of classdesc, and standalone gesture doc
     * @memberof oj.ojSwitch
     */
    
    /**
     * <table class="keyboard-table">
     *   <thead>
     *     <tr>
     *       <th>Target</th>
     *       <th>Key</th>
     *       <th>Action</th>
     *     </tr>
     *   </thead>
     *   <tbody>
     *     <tr>
     *       <td>Switch Thumb</td>
     *       <td><kbd>Enter</kbd> or <kbd>Space</kbd></td>
     *       <td>Toggle switch value</td>
     *     </tr>
     *   </tbody>
     * </table>
     *
     * <p>Disabled items can not receive keyboard focus.
     *
     * @ojfragment keyboardDoc - Used in keyboard section of classdesc, and standalone gesture doc
     * @memberof oj.ojSwitch
     */
			
});

}() );