blob: a4e000817934fae51d8209ea0d5bc0a30bd8cd9e [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2013, 2016 EclipseSource and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* EclipseSource - initial API and implementation
******************************************************************************/
(function() {
var TAB = String.fromCharCode( 9 );
var eventTypes = {
Selection : SWT.Selection,
DefaultSelection : SWT.DefaultSelection,
Show : SWT.Show,
Hide : SWT.Hide
};
var forwardedKeys = {
Enter : true,
Up : true,
Down : true,
PageUp : true,
PageDown : true,
Escape : true
};
namespace( "rwt.widgets" );
/**
* @class Instances of DropDown represent the server-side counterpart of a DropDown widget
*/
rwt.widgets.DropDown = function( args ) {
this._ = {};
this._.hScroll = !!args.hScroll;
this._.hideTimer = new rwt.client.Timer( 0 );
this._.hideTimer.addEventListener( "interval", checkFocus, this );
this._.parent = args.parent;
this._.appearance = args.appearance;
this._.customVariant = null;
this._.styleMap = null;
this._.popup = createPopup( args.appearance ); // TODO: create on demand
this._.grid = createGrid( this._.popup, !!args.markupEnabled, args.appearance );
inheritParentStyling.call( this );
this._.visibleItemCount = 5;
this._.items = [];
this._.columns = null;
this._.inMouseSelection = false;
this._.visibility = false;
this._.minWidth = 0;
this._.selectionWrapping = true;
this._.events = createEventsMap();
addParentListeners.call( this );
addGridListeners.call( this );
this._.popup.addEventListener( "appear", onAppear, this );
this._.parentFocusRoot = args.parent.getFocusRoot();
this._.parentFocusRoot.addEventListener( "changeFocusedChild", onFocusChange, this );
};
rwt.widgets.DropDown.prototype = {
classname : "rwt.widgets.DropDown",
setItems : function( items ) {
this.setSelectionIndex( -1 );
delete this._.maxTextWidth;
this._.items = rwt.util.Arrays.copy( items );
renderGridItems.call( this );
if( this._.grid.isSeeable() ) {
renderLayout.call( this );
}
if( this._.visibility && items.length > 0 ) {
this.show();
} else if( this._.visibility && items.length === 0 ) {
this._.popup.hide();
}
},
getItems : function() {
return rwt.util.Arrays.copy( this._.items );
},
getItemCount : function() {
return this._.grid.getRootItem().getChildrenLength();
},
/**
* Not intended to be called by ClientScripting
*/
setVisibleItemCount : function( itemCount ) {
this._.visibleItemCount = itemCount;
if( this._.grid.isSeeable() ) {
renderLayout.call( this );
}
// TODO: hide dropdown completely if no items are visible
},
getVisibleItemCount : function() {
return this._.visibleItemCount;
},
setSelectionIndex : function( index ) {
if( index < -1 || index >= this.getItemCount() || isNaN( index ) ) {
throw new Error( "Can not select item: Index " + index + " not valid" );
}
if( this.getSelectionIndex() === index ) {
return;
}
this._.grid.deselectAll();
if( index > -1 ) {
var item = this._.grid.getRootItem().getChild( index );
this._.grid.selectItem( item );
this._.grid.setFocusItem( item );
this._.grid.scrollItemIntoView( item );
} else {
this._.grid.setFocusItem( null );
this._.grid.setTopItemIndex( 0 );
}
// Not called for selection changes by API/Server:
this._.grid.dispatchSimpleEvent( "selectionChanged", { "type" : "selection" } );
},
getSelectionIndex : function() {
var selection = this._.grid.getSelection();
var result = -1;
if( selection[ 0 ] ) {
result = this._.grid.getRootItem().indexOf( selection[ 0 ] );
}
return result;
},
setVisible : function( value ) {
if( value ) {
this.show();
} else {
this.hide();
}
},
getVisible : function() {
return this._.visibility;
},
setMinWidth : function( value ) {
this._.minWidth = value;
if( this._.grid.isSeeable() ) {
renderLayout.call( this );
}
},
setSelectionWrapping : function( value ) {
this._.selectionWrapping = value;
},
setCustomVariant : function( value ) {
this._.customVariant = value;
this._.styleMap = null;
this._.popup.setCustomVariant( value );
this._.grid.setCustomVariant( value );
},
setDirection : function( value ) {
this._.grid.setDirection( value );
},
show : function() {
checkDisposed( this );
if( !this._.visibility ) {
this._.visibility = true;
addMouseWheelEventFilter.call( this );
fireEvent.call( this, "Show" );
}
if( this._.items.length > 0 && this._.parent.isSeeable() && !this._.popup.isSeeable() ) {
renderLayout.call( this );
setPopUpVisible.call( this, true );
}
},
hide : function() {
checkDisposed( this );
if( this._.visibility ) {
this._.visibility = false;
removeMouseWheelEventFilter.call( this );
fireEvent.call( this, "Hide" );
}
setPopUpVisible.call( this, false );
},
setData : function( key, value ) {
if( !this._.widgetData ) {
this._.widgetData = {};
}
if( arguments.length === 1 && key instanceof Object ) {
rwt.util.Objects.mergeWith( this._.widgetData, key );
} else {
this._.widgetData[ key ] = value;
}
},
getData : function( key ) {
if( !this._.widgetData ) {
return null;
}
var data = this._.widgetData[ key ];
return data === undefined ? null : data;
},
addListener : function( type, listener ) {
if( this._.events[ type ] ) {
if( this._.events[ type ].indexOf( listener ) === -1 ) {
this._.events[ type ].push( listener );
}
} else {
throw new Error( "Unkown type " + type );
}
},
removeListener : function( type, listener ) {
if( this._ && this._.events[ type ] ) {
var index = this._.events[ type ].indexOf( listener );
rwt.util.Arrays.removeAt( this._.events[ type ], index );
}
},
/**
* Experimental!
*/
setColumns : function( columns ) {
this._.columns = columns;
this._.grid.setColumnCount( columns.length );
this._.grid.setColumnOrder = function() {};
var config = this._.grid.getRenderConfig();
config.cellOrder = new Array( columns.length );
for( var i = 0; i < columns.length; i++ ) {
config.cellOrder[ i ] = i;
}
renderGridItems.call( this );
if( this._.grid.isSeeable() ) {
renderLayout.call( this );
}
},
/**
* Not intended to be called by ClientScripting
*/
destroy : function() {
if( !this.isDisposed() ) {
var parentFocusRoot = this._.parentFocusRoot;
if( parentFocusRoot && !parentFocusRoot.isDisposed() ) {
parentFocusRoot.removeEventListener( "changeFocusedChild", onFocusChange, this );
}
if( !this._.grid.isDisposed() ) {
this._.grid.getRootItem().setItemCount( 0 );
}
if( !this._.parent.isDisposed() ) {
this._.parent.removeEventListener( "appear", onParentVisibilityChange, this );
this._.parent.removeEventListener( "disappear", onParentVisibilityChange, this );
this._.parent.removeEventListener( "flush", onParentFlush, this );
this._.parent.removeEventListener( "keydown", onParentKeyDownEvent, this );
this._.parent.removeEventListener( "keypress", onParentKeyPressEvent, this );
this._.parent.removeEventListener( "changeFont", inheritParentStyling, this );
this._.parent.removeEventListener( "changeTextColor", inheritParentStyling, this );
this._.parent.removeEventListener( "changeBackgroundColor", inheritParentStyling, this );
this._.parent.removeEventListener( "changeCursor", inheritParentStyling, this );
}
removeMouseWheelEventFilter.call( this );
this._.popup.destroy();
this._.hideTimer.dispose();
if( this._.widgetData ) {
for( var key in this._.widgetData ) {
this._.widgetData[ key ] = null;
}
}
for( var key in this._ ) {
this._[ key ] = null;
}
this._ = null;
}
},
isDisposed : function() {
return this._ === null;
},
toString : function() {
return "DropDown";
},
applyObjectId : function( id ) {
this._.id = id;
this._.popup.applyObjectId( id );
}
};
////////////
// "statics"
rwt.widgets.DropDown.searchItems = function( items, query, limit ) {
var resultIndicies = [];
var filter = function( item, index ) {
if( query.test( item ) ) {
resultIndicies.push( index );
return true;
} else {
return false;
}
};
var resultLimit = typeof limit === "number" ? limit : 0;
var resultItems = filterArray( items, filter, resultLimit );
return {
"items" : resultItems,
"indicies" : resultIndicies,
"query" : query,
"limit" : resultLimit
};
};
rwt.widgets.DropDown.createQuery = function( str, caseSensitive, ignorePosition ) {
var escapedStr = rwt.widgets.DropDown.escapeRegExp( str );
return new RegExp( ( ignorePosition ? "" : "^" ) + escapedStr, caseSensitive ? "" : "i" );
};
rwt.widgets.DropDown.escapeRegExp = function( str ) {
return str.replace( /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&" );
};
////////////
// Internals
function addParentListeners() {
this._.parent.addEventListener( "appear", onParentVisibilityChange, this );
this._.parent.addEventListener( "disappear", onParentVisibilityChange, this );
this._.parent.addEventListener( "flush", onParentFlush, this );
this._.parent.addEventListener( "keydown", onParentKeyDownEvent, this );
this._.parent.addEventListener( "keypress", onParentKeyPressEvent, this );
this._.parent.addEventListener( "changeFont", inheritParentStyling, this );
this._.parent.addEventListener( "changeTextColor", inheritParentStyling, this );
this._.parent.addEventListener( "changeBackgroundColor", inheritParentStyling, this );
this._.parent.addEventListener( "changeCursor", inheritParentStyling, this );
}
function addGridListeners() {
this._.grid.addEventListener( "create", onCreate, this );
this._.grid.addEventListener( "selectionChanged", onSelection, this );
this._.grid.addEventListener( "keypress", onKeyEvent, this );
this._.grid.addEventListener( "mousedown", onMouseDown, this );
this._.grid.addEventListener( "mouseup", onMouseUp, this );
if ( this._.hScroll && !this._.columns ) {
this._.grid.getRowContainer().addEventListener( "rowRendered", onRowRendered, this );
}
}
function setPopUpVisible( visible ) {
if( visible ) {
this._.popup.show();
} else {
this._.popup.setVisibility( false ); // makes it disappear immediately
this._.popup.setDisplay( false ); // forces the popup to appear after all parents are layouted
}
}
function renderLayout() {
var font = this._.grid.getFont();
// NOTE: Guessing the lineheight to be 1.3
var padding = getStyleMap.call( this ).padding;
var itemHeight = Math.floor( font.getSize() * 1.3 ) + padding[ 0 ] + padding[ 2 ];
var visibleItems = Math.min( this._.visibleItemCount, this.getItemCount() );
var gridWidth = calcGridWidth.apply( this );
var gridHeight = calcGridHeight.apply( this, [ visibleItems, itemHeight ] );
var itemWidth = computeItemWidth.apply( this, [ gridWidth, padding ] );
renderPosition.call( this );
var frameWidth = getStyleMap.call( this ).border.getWidthLeft() * 2;
this._.popup.setWidth( gridWidth + frameWidth );
this._.popup.setHeight( gridHeight + frameWidth );
this._.grid.setDimension( gridWidth, gridHeight );
renderItemMetrics.apply( this, [ itemHeight, itemWidth, padding ] );
updateScrollBars.apply( this, [ gridWidth, itemWidth ] );
}
function renderPosition() {
this._.popup.positionRelativeTo( this._.parent, 0, this._.parent.getHeight() );
var viewportHeight = rwt.html.Viewport.getHeight();
var scrollTop = rwt.html.Viewport.getScrollTop();
if( this._.popup.getTop() + this._.popup.getHeight() > viewportHeight + scrollTop ) {
this._.popup.positionRelativeTo( this._.parent, 0, -1 * this._.popup.getHeight() );
}
}
function calcGridWidth() {
var frameWidth = getStyleMap.call( this ).border.getWidthLeft() * 2;
var result = this._.parent.getWidth() - frameWidth;
if( this._.minWidth > 0 ) {
var padding = getStyleMap.call( this ).padding;
var scrollbarWidth = 0;
if( this._.visibleItemCount < this.getItemCount() ) {
scrollbarWidth = this._.grid.getVerticalBar().getWidth();
}
var preferredWidth = this._.minWidth + padding[ 1 ] + padding[ 3 ] + scrollbarWidth;
result = Math.max( result, preferredWidth );
}
if( this._.columns ) {
var columnsSum = 0;
for( var i = 0; i < this._.columns.length; i++ ) {
columnsSum += this._.columns[ i ];
}
if( columnsSum > result ) {
result = columnsSum;
}
}
return result;
}
function calcGridHeight( visibleItems, itemHeight ) {
var result = visibleItems * itemHeight;
if ( this._.hScroll ) {
result += this._.grid.getHorizontalBar().getHeight();
}
return result;
}
function renderItemMetrics( itemHeight, itemWidth, padding ) {
this._.grid.setItemHeight( itemHeight );
if( this._.columns != null ) {
var left = 0;
for( var i = 0; i < this._.columns.length; i++ ) {
var column = this._.columns[ i ];
this._.grid.setItemMetrics(
i, // column
left, // left
column, // width
0, // imageLeft
0, // imageWidth
left + padding[ 3 ], // textLeft
column - padding[ 1 ] - padding[ 3 ], // textWidth
0, // checkLeft
0 // checkWith
);
left += column;
}
} else {
this._.grid.setItemMetrics(
0, // column
0, // left
itemWidth, // width
0, // imageLeft
0, // imageWidth
padding[ 3 ], // textLeft
itemWidth - padding[ 1 ] - padding[ 3 ], // textWidth
0, // checkLeft
0 // checkWith
);
}
}
function computeItemWidth( gridWidth, padding ) {
var neededWidth = this._.hScroll ? ( this._.maxTextWidth || 0 ) + padding[ 1 ] + padding[ 3 ] : 0;
return Math.max( neededWidth, gridWidth );
}
function renderGridItems() {
var rootItem = this._.grid.getRootItem();
var items = this._.items;
rootItem.setItemCount( 0 );
rootItem.setItemCount( items.length );
for( var i = 0; i < items.length; i++ ) {
var gridItem = new rwt.widgets.GridItem( rootItem, i, false );
gridItem.applyObjectId( this._.id + "-listitem-" + i );
if( this._.columns ) {
gridItem.setTexts( items[ i ].split( TAB ) );
} else {
gridItem.setTexts( [ items[ i ] ] );
}
}
}
function onParentVisibilityChange() {
if( this._.visibility ) {
if( this._.parent.isSeeable() ) {
this.show(); // makes popup visible if items are present and handles layout
} else {
setPopUpVisible.call( this, false );
}
}
}
function onParentKeyDownEvent( event ) {
// NOTE: This prevents the underlying Shell from closing. Shell is listening for keydown.
var key = event.getKeyIdentifier();
if( this._.visibility && ( key === "Enter" || key === "Escape" ) ) {
event.stopPropagation();
}
}
function onParentKeyPressEvent( event ) {
var key = event.getKeyIdentifier();
if( this._.visibility && forwardedKeys[ key ] && !event.isAltPressed() ) {
event.preventDefault();
if( this._.selectionWrapping ) {
selectWithWrapping.call( this, event );
} else {
selectWithoutWrapping.call( this, event );
}
}
}
function selectWithWrapping( event ) {
var key = event.getKeyIdentifier();
if( key === "Down" && this.getSelectionIndex() === -1 && this.getItemCount() > 0 ) {
this.setSelectionIndex( 0 );
} else if( key === "Up" && this.getSelectionIndex() === 0 ) {
this.setSelectionIndex( -1 );
} else if( key === "Down" && this.getSelectionIndex() === this.getItemCount() - 1 ) {
this.setSelectionIndex( -1 );
} else if( key === "Up" && this.getSelectionIndex() === -1 && this.getItemCount() > 0 ) {
this.setSelectionIndex( this.getItemCount() - 1 );
} else {
this._.grid.dispatchEvent( event );
}
}
function selectWithoutWrapping( event ) {
var key = event.getKeyIdentifier();
var allowSelection = this.getSelectionIndex() === -1 && this.getItemCount() > 0;
if( ( key === "Down" || key === "PageDown" ) && allowSelection ) {
this.setSelectionIndex( 0 );
} else if( ( key === "Up" || key === "PageUp" ) && allowSelection ) {
this.setSelectionIndex( this.getItemCount() - 1 );
} else {
this._.grid.dispatchEvent( event );
}
}
function onParentFlush( event ) {
var changes = event.getData();
var layouted = changes.top || changes.left || changes.width || changes.height;
if( layouted && this._.parent.isInDom() && this._.visibility ) {
renderLayout.call( this );
}
}
function onKeyEvent( event ) {
switch( event.getKeyIdentifier() ) {
case "Enter":
rwt.client.Timer.once( function() {
// NOTE : This async call ensures that the key events is processed before the
// DefaultSelection event. A better solution would be to do this for all forwarded
// key events, but this would be complicated since the event is disposed by the
// time dispatch would be called on the grid.
fireEvent.call( this, "DefaultSelection" );
}, this, 0 );
break;
case "Escape":
this.hide();
break;
}
}
function onSelection( event ) {
if( event.type === "selection" ) {
fireEvent.call( this, "Selection" );
}
}
function onMouseDown( event ) {
if( event.getOriginalTarget() instanceof rwt.widgets.base.GridRowContainer ) {
this._.inMouseSelection = true;
}
}
function onMouseUp( event ) {
if( this._.inMouseSelection
&& event.getOriginalTarget() instanceof rwt.widgets.base.GridRowContainer )
{
this._.inMouseSelection = false;
fireEvent.call( this, "DefaultSelection" );
}
}
function onCreate() {
var selectedItem = this._.grid.getSelection()[ 0 ];
if( selectedItem ) {
this._.grid.scrollItemIntoView( selectedItem );
}
}
function onAppear() {
// NOTE: widget absolute position can change without changing it's relative postion, therefore:
renderPosition.call( this );
}
function onFocusChange() {
// NOTE : There is no secure way to get the newly focused widget at this point because
// it may have another focus root. Therefore we use this timeout and check afterwards:
this._.hideTimer.start();
}
function onRowRendered( row ) {
var textWidth = row._computeVisualTextWidth( 0 );
if ( textWidth > ( this._.maxTextWidth || 0 ) ) {
this._.maxTextWidth = textWidth;
renderLayout.call( this );
}
}
function fireEvent( type ) {
var event = {
"text" : "",
"index" : -1
};
if( type === "Selection" || type === "DefaultSelection" ) {
var selection = this._.grid.getSelection();
if( selection.length > 0 ) {
event.index = this.getSelectionIndex();
event.text = this._.items[ event.index ];
}
notify.apply( this, [ type, event ] );
if( !rwt.remote.EventUtil.getSuspended() ) { // TODO [tb] : ClientScripting must reset flag
if( type === "DefaultSelection" && selection.length > 0 ) {
this.hide();
}
}
} else {
notify.apply( this, [ type, event ] );
}
}
function checkFocus() {
this._.hideTimer.stop();
if( !hasFocus( this._.parent ) && this._.visibility ) {
this.hide();
}
}
function updateScrollBars( gridWidth, itemWidth ) {
var vScrollable = this._.visibleItemCount < this.getItemCount();
var hScrollable = this._.hScroll && gridWidth < itemWidth;
this._.grid.setScrollBarsVisible( hScrollable, vScrollable );
}
function notify( type, event ) {
var listeners = this._.events[ type ];
var eventProxy = rwt.util.Objects.mergeWith( {
"widget" : this,
"type" : eventTypes[ type ]
}, event );
for( var i = 0; i < listeners.length; i++ ) {
listeners[ i ]( eventProxy );
}
}
function createPopup( appearance ) {
var result = new rwt.widgets.base.Popup();
result.addToDocument();
result.setBackgroundColor( "#ffffff" );
result.setDisplay( false );
result.setRestrictToPageOnOpen( false );
result.setAutoHide( false );
result.setAppearance( appearance + "-popup" );
return result;
}
function createGrid( parent, markupEnabled, appearance ) {
var result = new rwt.widgets.Grid( {
"fullSelection" : true,
"appearance" : appearance,
"markupEnabled" : markupEnabled
} );
result.setLocation( 0, 0 );
result.setParent( parent );
result.setTreeColumn( -1 ); // TODO [tb] : should be default?
result.setScrollBarsVisible( false, false );
result.getRenderConfig().focused = true;
result.addEventListener( "changeFocused", function() {
result.getRenderConfig().focused = true;
} );
return result;
}
function inheritParentStyling() {
this._.grid.setFont( this._.parent.getFont() );
this._.grid.setTextColor( this._.parent.getTextColor() );
this._.grid.setBackgroundColor( this._.parent.getBackgroundColor() );
// [if] "default" fallback is needed to suppress ibeam cursor shown in Chrome and IE, when no
// cursor (null) is set - bug 434311
this._.grid.setCursor( this._.parent.getCursor() || "default" );
}
function checkDisposed( dropdown ) {
if( dropdown.isDisposed() ) {
throw new Error( "DropDown is disposed" );
}
}
function createEventsMap() {
var result = {};
for( var key in eventTypes ) {
result[ key ] = [];
}
return result;
}
function hasFocus( control ) {
var root = control.getFocusRoot();
if( root ) {
return control.getFocused()
|| ( control.contains && control.contains( root.getFocusedChild() ) );
}
return false;
}
function filterArray( arr, func, limit ) {
var result = [];
if( typeof arr.filter === "function" && limit === 0 ) {
result = arr.filter( func );
} else {
for( var i = 0; i < arr.length; i++ ) {
if( func( arr[ i ], i ) ) {
result.push( arr[ i ] );
if( limit !== 0 && result.length === limit ) {
break;
}
}
}
}
return result;
}
function getStyleMap() {
if( this._.styleMap == null ) {
var manager = rwt.theme.AppearanceManager.getInstance();
var states = {};
if( this._.customVariant ) {
states[ this._.customVariant ] = true;
}
this._.styleMap = {
"border" : manager.styleFrom( this._.appearance + "-popup", states ).border,
"padding" : manager.styleFrom( this._.appearance + "-cell", states ).padding
};
}
return this._.styleMap;
}
function addMouseWheelEventFilter() {
rwt.event.EventHandler.setMouseEventFilter( filterMouseEvent, this );
}
function removeMouseWheelEventFilter() {
var currentFilter = rwt.event.EventHandler.getMouseEventFilter();
if( currentFilter && currentFilter[ 0 ] === filterMouseEvent && currentFilter[ 1 ] === this ) {
rwt.event.EventHandler.setMouseEventFilter( null );
}
}
function filterMouseEvent( event ) {
if( event.getType() === "mousedown" ) {
var target = event.getTarget();
if( target !== this._.popup && !this._.popup.contains( target )
&& target !== this._.parent && !this._.parent.contains( target ) )
{
this.hide();
}
} else if( event.getType() === "mousewheel" ) {
event.preventDefault();
this._.grid.getRowContainer().dispatchEvent( event );
return false;
}
return true;
}
}() );