/**
* Copyright (c) 2014, Oracle and/or its affiliates.
* All rights reserved.
*/
/**
* @ojcomponent oj.ojRowExpander
* @augments oj.baseComponent
*
* @classdesc
* <h3 id="rowexpanderOverview-section">
* JET RowExpander Component
* <a class="bookmarkable-link" title="Bookmarkable Link" href="#rowexpanderOverview-section"></a>
* </h3>
* <p>Description: A JET RowExpander is a component that is primarily used inside the JET Table and JET DataGrid widgets. It enables hierarchical data to be display in a JET Table and JET DataGrid.</p>
*
* <p>To enable expand and collapse of rows, developers must specify oj.FlattenedTreeTableDataSource as data when used within JET Table and oj.FlattenedTreeDataGridDataSource as data when used within JET DataGrid.</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="rtl-section">
* Reading direction
* <a class="bookmarkable-link" title="Bookmarkable Link" href="#rtl-section"></a>
* </h3>
*
* <p>The location of the row expander will be reversed in RTL reading direction.</p>
* <p>As with any JET component, in the unusual case that the directionality (LTR or RTL) changes post-init, the component containing the row expander (JET Table or JET DataGrid) must be <code class="prettyprint">refresh()</code>ed.
*/
oj.__registerWidget('oj.ojRowExpander', $['oj']['baseComponent'],
{
version: "1.0.0",
widgetEventPrefix: 'oj',
options:
{
/**
* The context object obtained from the column renderer (Table) or cell renderer (DataGrid)
*
* @expose
* @memberof! oj.ojRowExpander
* @instance
* @type {Object}
* @default <code class="prettyprint">null</code>
*
*/
context: null,
/**
* Triggered when a expand is performed on the row expander
*
* @expose
* @event
* @memberof! oj.ojRowExpander
* @instance
* @property {Event} event <code class="prettyprint">jQuery</code> event object
* @property {Object} ui Parameters
* @property {string} ui.rowKey the key of the row expanded
*
* @example <caption>Initialize the row expander with the <code class="prettyprint">expand</code> callback specified:</caption>
* $( ".selector" ).ojRowExpander({
* "expand": function( event, ui ) {}
* });
*
* @example <caption>Bind an event listener to the <code class="prettyprint">ojexpand</code> event:</caption>
* $( ".selector" ).on( "ojexpand", function( event, ui ) {} );
*/
expand: null,
/**
* Triggered when a collapse is performed on the row expander
*
* @expose
* @event
* @memberof! oj.ojRowExpander
* @instance
* @property {Event} event <code class="prettyprint">jQuery</code> event object
* @property {Object} ui Parameters
* @property {string} ui.rowKey the key of the row collapsed
*
* @example <caption>Initialize the row expander with the <code class="prettyprint">collapse</code> callback specified:</caption>
* $( ".selector" ).ojRowExpander({
* "collapse": function( event, ui ) {}
* });
*
* @example <caption>Bind an event listener to the <code class="prettyprint">ojcollapse</code> event:</caption>
* $( ".selector" ).on( "ojcollapse", function( event, ui ) {} );
*/
collapse: null
},
classNames:
{
'root': 'oj-rowexpander',
'icon': 'oj-component-icon',
'clickable': 'oj-clickable-icon-nocontext',
'expand': 'oj-rowexpander-expand-icon',
'collapse': 'oj-rowexpander-collapse-icon',
'leaf': 'oj-rowexpander-leaf-icon',
'lazyload': 'oj-rowexpander-lazyload-icon',
'toucharea': 'oj-rowexpander-touch-area',
'indent': 'oj-rowexpander-indent',
'iconspacer': 'oj-rowexpander-icon-spacer',
'depth0': 'oj-rowexpander-depth-0',
'depth1': 'oj-rowexpander-depth-1',
'depth2': 'oj-rowexpander-depth-2',
'depth3': 'oj-rowexpander-depth-3',
'depth4': 'oj-rowexpander-depth-4',
'depth5': 'oj-rowexpander-depth-5',
'depth6': 'oj-rowexpander-depth-6',
'depth7': 'oj-rowexpander-depth-7'
},
constants :{
MAX_STYLE_DEPTH: 7,
NUM5_KEY: 53
},
/**
* Create the row expander
* @override
* @memberof! oj.ojRowExpander
* @protected
*/
_ComponentCreate: function()
{
this._super();
this.element.addClass(this.classNames['root']);
this._initContent();
},
/**
* Initialize the row expander after creation
* @private
*/
_initContent : function ()
{
var self = this, context;
context = this.options['context'];
this.component = context['component'];
this.datasource = context['datasource'];
//root hidden so subtract 1
this.depth = context['depth'];
this.iconState = context['state'];
this.rowKey = context['key'];
this.index = context['index'];
this.parentKey = context['parentKey'];
this._addIndentation();
this._addIcon();
this._setIconStateClass();
if (this.iconState === 'expanded' || this.iconState === 'collapsed')
{
$(this.toucharea).on('touchend', function(event) {
//prevent scroll to top and # append, also prevents the following click
event.preventDefault();
self._fireExpandCollapse();
});
$(this.toucharea).on('click', function(event) {
//prevent scroll to top and # append
event.preventDefault();
self._fireExpandCollapse();
});
$(this.element).on('keypress', function(event) {
var code = event.keyCode || event.which;
if (code === $.ui.keyCode.ENTER || code === $.ui.keyCode.SPACE)
{
// do expand or collapse
self._fireExpandCollapse();
// stop browser from for example scrolling the page
event.preventDefault();
// ensure focus stays
event.target.focus();
}
});
// listen for key down event from host component
this.handleKeyDownCallback = this._handleKeyDownEvent.bind(this);
$(this.component.element).on('ojkeydown', this.handleKeyDownCallback);
// listens for expand and collapse event from flattened datasource
// this could be due to user clicks, keyboard shortcuts or programmatically
this.handleExpandCallback = this._handleExpandEvent.bind(this);
this.handleCollapseCallback = this._handleCollapseEvent.bind(this);
this.datasource.on("expand", this.handleExpandCallback, this);
this.datasource.on("collapse", this.handleCollapseCallback, this);
}
else if (this.iconState === 'leaf')
{
// we'll still need to handle ctrl+alt+5 for leaf node
// listen for key down event from host component
this.handleKeyDownCallback = this._handleKeyDownEvent.bind(this);
$(this.component.element).on('ojkeydown', this.handleKeyDownCallback);
$(this.icon).attr('tabindex', -1);
}
// listen for active key change event from host component
this.handleActiveKeyChangeCallback = this._handleActiveKeyChangeEvent.bind(this);
$(this.component.element).on('ojactive', this.handleActiveKeyChangeCallback);
},
/**
* Refresh the row expander having made external modifications
*
* <p>This method does not accept any arguments.
*
* @expose
* @memberof! oj.ojRowExpander
* @instance
*
* @example <caption>Invoke the <code class="prettyprint">refresh</code> method:</caption>
* $( ".selector" ).ojRowExpander( "refresh" );
*/
refresh: function()
{
this.element.empty();
this._initContent();
},
/**
* destroy the row expander
*
* <p>This method does not accept any arguments.
*
* @expose
* @memberof! oj.ojRowExpander
* @instance
* @private
* @example <caption>Invoke the <code class="prettyprint">refresh</code> method:</caption>
* $( ".selector" ).ojRowExpander( "destroy" );
*/
_destroy: function()
{
// unregister keydown and active key change handlers
$(this.component.element).off('ojkeydown', this.handleKeyDownCallback);
$(this.component.element).off('ojactive', this.handleActiveKeyChangeCallback);
// unregister expand/collapse events
this.datasource.off("expand", this.handleExpandCallback, this);
this.datasource.off("collapse", this.handleCollapseCallback, this);
this.element.removeClass(this.classNames['root']);
this.element.empty();
},
/**
* Sets a single option value
* @param {Object} key the option key
* @param {Object} value the option value
* @param {Object} flags additional flags for option*
* @override
* @private
*/
_setOption: function(key, value, flags)
{
this._super(key, value, flags);
// refresh if context is updated
if (key == 'context')
{
this.refresh();
}
},
/**
* Add athe indentation spacers to the row
* @private
*/
_addIndentation: function()
{
var remainder, i, depth;
//0 index the depth for style purposes
depth = this.depth-1;
if (depth < this.constants.MAX_STYLE_DEPTH)
{
this._appendSpacer(depth);
}
else
{
for (i=1; i <= (depth/(this.constants.MAX_STYLE_DEPTH)); i++)
{
this._appendSpacer(this.constants.MAX_STYLE_DEPTH);
}
remainder = (depth % this.constants.MAX_STYLE_DEPTH);
if (remainder < this.constants.MAX_STYLE_DEPTH)
{
this._appendSpacer(remainder);
}
}
},
/**
* Append appropriate spacer based on depth to the row expander
* @param {number} depth the depth
* @private
*/
_appendSpacer: function(depth)
{
var spacer = $(document.createElement('span')).addClass(this.classNames['indent']).addClass(this.classNames['depth'+depth]);
this.element.append(spacer); //@HTMLUpdateOK
},
/**
* Add an icon to the row expander with appropriate class names for a clickable icon.
* @private
*/
_addIcon: function()
{
var iconSpacer = $(document.createElement('div')).addClass(this.classNames['iconspacer']);
this.toucharea = $(document.createElement('div')).addClass(this.classNames['toucharea']);
this.icon = $(document.createElement('a')).attr('href', '#').attr('aria-labelledby', this._getLabelledBy()).addClass(this.classNames['icon']).addClass(this.classNames['clickable']);
this.element.append(iconSpacer.append(this.toucharea.append(this.icon))); //@HTMLUpdateOK
},
/**
* Add a class name on the icon
* @private
* @param {string} classKey the key of the appropriate icon class expand/collapse/leaf
*/
_addIconClass: function(classKey)
{
this.icon.addClass(this.classNames[classKey]);
},
/**
* Remove a class name on the icon
* @private
* @param {string} classKey the key of the appropriate icon class expand/collapse/leaf
*/
_removeIconClass: function(classKey)
{
this.icon.removeClass(this.classNames[classKey]);
},
/**
* Set the icon class to the the iconState property
* @private
*/
_setIconStateClass: function()
{
switch (this.iconState)
{
case 'leaf':
this._removeIconClass('icon');
this._removeIconClass('clickable');
this._addIconClass('leaf');
break;
case 'collapsed':
this._addIconClass('expand');
this._ariaExpanded(false);
break;
case 'expanded':
this._addIconClass('collapse');
this._ariaExpanded(true);
break;
case 'loading':
this._removeIconClass('clickable');
this._addIconClass('lazyload');
break;
}
},
/**
* Removes the icon class of the iconState property
* @private
*/
_removeIconStateClass: function()
{
switch (this.iconState)
{
case 'leaf':
this._removeIconClass('leaf');
this._addIconClass('icon');
this._addIconClass('clickable');
break;
case 'collapsed':
this._removeIconClass('expand');
break;
case 'expanded':
this._removeIconClass('collapse');
break;
case 'loading':
this._removeIconClass('lazyload');
this._addIconClass('clickable');
break;
}
},
/**
* Handles active key change event from host component (ojDataGrid or ojTable)
* @param {Event} event
* @param {Object} ui
* @private
*/
_handleActiveKeyChangeEvent: function(event, ui)
{
var rowKey, context, state;
if (ui['activeKey'] != null)
{
rowKey = ui['activeKey']['rowKey'];
// if the event is for this row and the active key change event is triggered
// by row change and not column change
if (this.rowKey === rowKey && (ui['previousActiveKey'] == null || ui['previousActiveKey']['row'] != ui['activeKey']['row']))
{
// if the component allows AccessibleContext to be set
if (this.component._setAccessibleContext)
{
// row context of row expander for screen reader
// todo: get index from TreeDataSource as well since that could change
context = this.getTranslatedString('accessibleRowDescription', {'level': this.depth, 'num': this.index+1, 'total': this.datasource.getWrappedDataSource().getChildCount(this.parentKey)});
// state of row expander for screen reader
if (this.iconState === 'collapsed')
{
state = this.getTranslatedString('accessibleStateCollapsed');
}
else if (this.iconState === 'expanded')
{
state = this.getTranslatedString('accessibleStateExpanded');
}
else
{
// for leaf node don't read anything
state = '';
}
this.component._setAccessibleContext({'context': context, 'state': state});
}
}
}
},
/**
* Handles keydown event from host component (ojDataGrid or ojTable)
* @param {Event} event
* @param {Object} ui
* @private
*/
_handleKeyDownEvent: function(event, ui)
{
var rowKey, code, context, ancestorInfo, ancestors, i;
rowKey = ui['rowKey'];
if (this.rowKey === rowKey)
{
event = event['originalEvent'];
code = event.keyCode || event.which;
// ctrl (or equivalent) is pressed
if (oj.DomUtils.isMetaKeyPressed(event))
{
// Ctrl+Right expands, Ctrl+Left collapse in accordance with WAI-ARIA best practice
// consume the event as it's processed
if (code == $.ui.keyCode.RIGHT && this.iconState === 'collapsed')
{
this._loading();
this.datasource.expand(this.rowKey);
event.preventDefault();
}
else if (code == $.ui.keyCode.LEFT && this.iconState === 'expanded')
{
this._loading();
this.datasource.collapse(this.rowKey);
event.preventDefault();
}
else if (event.altKey && code == this.constants.NUM5_KEY)
{
// read current cell context
if (this.component._setAccessibleContext)
{
ancestors = this.datasource.getAncestors(this.rowKey);
if (ancestors != null && ancestors.length > 0)
{
ancestorInfo = [];
for (i=0; i<ancestors.length; i++)
{
ancestorInfo.push({'key': ancestors[i], 'label': this.getTranslatedString('accessibleLevelDescription', {'level': i+1})});
}
}
context = this.getTranslatedString('accessibleRowDescription', {'level': this.depth, 'num': this.index+1, 'total': this.datasource.getWrappedDataSource().getChildCount(this.parentKey)});
this.component._setAccessibleContext({'context': context, 'state': '', 'ancestors': ancestorInfo});
event.preventDefault();
}
}
}
}
},
/**
* Put row expander in a loading state. This is called during expand/collapse.
* @private
*/
_loading: function()
{
this._removeIconStateClass();
this.iconState = 'loading';
this._setIconStateClass();
},
/**
* Handle an expand event coming from the datasource,
* update the icon and the aria-expand property
* @param {Object} event the expand event from the data source, should contain rowKey
* @private
*/
_handleExpandEvent: function(event)
{
var rowKey = event['rowKey'];
if (rowKey === this.rowKey)
{
this._removeIconStateClass();
this.iconState = 'expanded';
this._setIconStateClass();
this._ariaExpanded(true);
this._trigger('expand', null, {'rowKey': rowKey});
}
},
/**
* Handle a collapse event coming from the datasource,
* update the icon and the aria-expand property
* @param {Object} event the collapse event from the data source, should contain rowKey
* @private
*/
_handleCollapseEvent: function(event)
{
var rowKey = event['rowKey'];
if (rowKey === this.rowKey)
{
this._removeIconStateClass();
this.iconState = 'collapsed';
this._setIconStateClass();
this._ariaExpanded(false);
this._trigger('collapse', null, {'rowKey': rowKey});
}
},
/**
* Fire the expand or collapse on the datasource and the oj event on the widget
* @private
*/
_fireExpandCollapse: function()
{
var state = this.iconState;
// show loading icon, note this changes the icon state to 'loading'
this._loading();
// invoke expand/collapse on datasource
if (state === 'collapsed')
{
this.datasource.expand(this.rowKey);
}
else if (state === 'expanded')
{
this.datasource.collapse(this.rowKey);
}
},
/**
* Sets the icon's aria-expanded property to the boolean passed in
* @param {boolean|null} bool true if expanded false if not
* @private
*/
_ariaExpanded: function(bool)
{
this.icon.attr('aria-expanded', bool);
},
/**
* Return the subcomponent node represented by the documented locator attribute values.
* <p>
* To lookup the expand/collapse icon the locator object should have the following:
* <ul>
* <li><b>subId</b>: 'oj-rowexpander-icon'</li>
* </ul>
*
* @expose
* @memberof! oj.ojRowExpander
* @instance
* @override
* @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:<p>
* component: optional - in the future there may be more than one
* component contained within a page element<p>
* subId: the string, documented by the component, that the component
* expects in getNodeBySubId to locate a particular subcomponent
* @returns {Element|null} the subcomponent located by the subId string passed
* in locator, if found.<p>
*/
getNodeBySubId: function(locator)
{
var subId;
if (locator == null)
{
return this.element ? this.element[0] : null;
}
subId = locator['subId'];
if (subId === 'oj-rowexpander-icon' && this.icon != null)
{
return this.icon.get(0);
}
// Non-null locators have to be handled by the component subclasses
return null;
},
/**
* Returns the subId string for the given child DOM node. For more details, see
* <a href="#getNodeBySubId">getNodeBySubId</a>.
*
* @expose
* @override
* @memberof oj.ojRowExpander
* @instance
*
* @param {!Element} node - child DOM node
* @return {Object|null} The subId for the DOM node, or <code class="prettyprint">null</code> when none is found.
*
* @example <caption>Get the subId for a certain DOM node:</caption>
* var subId = $( ".selector" ).ojRowExpander( "getSubIdByNode", nodeInsideComponent );
*/
getSubIdByNode: function(node)
{
if (node === this.icon.get(0))
{
return {subId: 'oj-rowexpander-icon'};
}
return null;
},
_NotifyAttached: function()
{
this._super();
this.icon.attr('aria-labelledby', this._getLabelledBy());
},
/**
* Get the aria label of the rowexpander from the closest row expander
* @return {string} the closest id set to the rowexpander
* @private
*/
_getLabelledBy: function()
{
return this.element.parent().closest('[id]').attr('id');
}
////////////////// FRAGMENTS //////////////////
/**
* <table class="keyboard-table">
* <thead>
* <tr>
* <th>Target</th>
* <th>Gesture</th>
* <th>Action</th>
* </tr>
* </thead>
* <tbody>
* <tr>
* <td>Icon</td>
* <td><kbd>Tap</kbd></td>
* <td>Expand or collapse the row with the icon in it.</td>
* </tr>
* </tbody>
* </table>
*
* @ojfragment touchDoc - Used in touch section of classdesc, and standalone gesture doc
* @memberof oj.ojRowExpander
*/
/**
* <table class="keyboard-table">
* <thead>
* <tr>
* <th>Target</th>
* <th>Key</th>
* <th>Action</th>
* </tr>
* </thead>
* <tbody>
* <tr>
* <td rowspan="2">Focused Row or Cell with Row Expander</td>
* <td><kbd>Ctrl + RightArrow</kbd></td>
* <td>Expand</td>
* </tr>
* <tr>
* <td><kbd>Ctrl + LeftArrow</kbd></td>
* <td>Collapse</td>
* </tr>
* <tr>
* <td rowspan="1">Icon</td>
* <td><kbd>Enter</kbd></td>
* <td>Expand or Collapse</td>
* </tr>
* </tbody>
* </table>
* @ojfragment keyboardDoc - Used in keyboard section of classdesc, and standalone gesture doc
* @memberof oj.ojRowExpander
*/
////////////////// SUB-IDS //////////////////
/**
* <p>Sub-ID for the ojRowExpander component's icon. See the <a href="#getNodeBySubId">getNodeBySubId</a>
* method for details.</p>
*
* @ojsubid
* @member
* @name oj-rowexpander-icon
* @memberof oj.ojRowExpander
* @instance
*
* @example <caption>Get the icon from the row expander component:</caption>
* var node = $( ".selector" ).ojRowExpander( "getNodeBySubId", {'subId': 'oj-rowexpander-icon'} );
*/
});
Source: src/main/javascript/oracle/oj/ojrowexpander/ojrowexpander.js
Oracle® JavaScript Extension Toolkit (JET)
1.1.2
E65298-01