package fr.lteconsulting.hexa.client.ui.treetable;
import java.util.ArrayList;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.ImageElement;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.ui.IsWidget;
import com.google.gwt.user.client.ui.Widget;
import fr.lteconsulting.hexa.client.interfaces.IAsyncCallback;
import fr.lteconsulting.hexa.client.tools.JQuery;
import fr.lteconsulting.hexa.client.ui.miracle.Printer;
import fr.lteconsulting.hexa.client.ui.miracle.Size;
public class Row
{
private final TreeTable treeTable;
Row( TreeTable treeTable )
{
this.treeTable = treeTable;
}
Row m_parent = null;
Element m_tr = null;
Element m_trToDelete = null;
ArrayList<Row> m_childs;
boolean m_fExpanded = true;
int m_ref = -1; // this field is synchronized with the dom element
// m_tr's "ref" attribute
Object m_dataObject = null;
private SafeHtml getExpandImageHtml()
{
StringBuilder sb = new StringBuilder();
sb.append( "<img src='" );
if( !hasChilds() )
sb.append( "" );
else if( getExpanded() )
sb.append( this.treeTable.treeMinus.getSafeUri().asString() );
else
sb.append( this.treeTable.treePlus.getSafeUri().asString() );
sb.append( "'></img>" );
return SafeHtmlUtils.fromTrustedString( sb.toString() );
}
private void updateExpandImage()
{
if( m_tr == null )
return;
Element td = m_tr.getChild( 0 ).cast();
if( td.getChildCount() == 0 )
return;
ImageElement img = td.getChild( 0 ).cast();
if( !hasChilds() )
img.setSrc( "" );
else if( getExpanded() )
img.setSrc( this.treeTable.treeMinus.getSafeUri().asString() );
else
img.setSrc( this.treeTable.treePlus.getSafeUri().asString() );
}
public Row getParent()
{
return m_parent == this.treeTable.m_rootItem ? null : m_parent;
}
public Cell getCell( int column )
{
return new Cell( column );
}
// adds an item at the end of the children of the parent
public Row addLastChild()
{
assert (this.treeTable.m_nbColumns > 0) : "Table should have at least one column before adding items";
Row newItem = new Row( this.treeTable );
newItem.m_tr = DOM.createTR();
newItem.m_tr.setPropertyObject( "linkedItem", newItem );
newItem.m_tr.setInnerHTML( this.treeTable.m_rowTemplate );
// DOM add
Row lastParentLeaf = getLastLeaf();
Element trToInsertAfter = lastParentLeaf.m_tr;
if( trToInsertAfter != null )
{
int after = DOM.getChildIndex( this.treeTable.m_body, trToInsertAfter );
int before = after + 1;
DOM.insertChild( this.treeTable.m_body, newItem.m_tr, before );
}
else
{
DOM.appendChild( this.treeTable.m_body, newItem.m_tr );
}
// logical add
newItem.m_parent = this;
getChilds().add( newItem );
signalStateChange();
// take care of the left padding
Element firstTd = DOM.getChild( newItem.m_tr, 0 );
firstTd.getStyle().setPaddingLeft( newItem.getLevel() * this.treeTable.treePadding, Unit.PX );
return newItem;
}
// move to be the last item of parent
public void moveLastChild( Row newParent )
{
if( this == newParent )
return;
// remove from its current position
Row parentItem = m_parent;
if( parentItem == null )
parentItem = this.treeTable.m_rootItem;
parentItem.getChilds().remove( this );
// DOM.removeChild( m_body, m_tr );
if( newParent == null )
newParent = this.treeTable.m_rootItem;
// insert at the end of the current parent
// DOM add
Row lastLeaf = newParent.getLastLeaf();
Element trToInsertAfter = lastLeaf.m_tr;
if( trToInsertAfter != null )
{
int after = DOM.getChildIndex( this.treeTable.m_body, trToInsertAfter );
int before = after + 1;
DOM.insertChild( this.treeTable.m_body, m_tr, before );
}
else
{
DOM.appendChild( this.treeTable.m_body, m_tr );
}
parentItem.getChilds().add( this );
// take care of the left padding
Element firstTd = DOM.getChild( m_tr, 0 );
firstTd.getStyle().setPaddingLeft( getLevel() * this.treeTable.treePadding, Unit.PX );
}
// adds a new sibling item, just below (with same parent)
public Row addBefore()
{
assert (this.treeTable.m_nbColumns > 0);
// which is the parent ? => same parent as item
Row parentItem = m_parent;
if( parentItem == null )
parentItem = this.treeTable.m_rootItem;
Row newItem = new Row( this.treeTable );
newItem.m_tr = Document.get().createTRElement();
newItem.m_tr.setPropertyObject( "linkedItem", newItem );
newItem.m_tr.setInnerHTML( this.treeTable.m_rowTemplate );
// DOM add
this.treeTable.m_body.insertBefore( newItem.m_tr, m_tr );
// logical add
newItem.m_parent = parentItem;
int itemPos = parentItem.getChilds().indexOf( this );
parentItem.getChilds().add( itemPos, newItem );
parentItem.signalStateChange();
// take care of the left padding
Element firstTd = DOM.getChild( newItem.m_tr, 0 );
firstTd.getStyle().setPaddingLeft( newItem.getLevel() * this.treeTable.treePadding, Unit.PX );
return newItem;
}
// move to position just before "item"
public void moveBefore( Row item )
{
if( this == item )
return;
Element lastTrToMove = getLastLeafTR();
Element firstChildRow = DOM.getNextSibling( m_tr );
// remove from its current position
Row parentItem = m_parent;
if( parentItem == null )
parentItem = this.treeTable.m_rootItem;
parentItem.getChilds().remove( this );
// insert at the selected position
if( item == null )
{
// insert at the end of the current parent
// DOM add
Row lastLeaf = parentItem.getLastLeaf();
Element trToInsertAfter = lastLeaf.m_tr;
if( trToInsertAfter != null )
{
int after = DOM.getChildIndex( this.treeTable.m_body, trToInsertAfter );
int before = after + 1;
DOM.insertChild( this.treeTable.m_body, m_tr, before );
}
else
{
DOM.appendChild( this.treeTable.m_body, m_tr );
}
parentItem.getChilds().add( this );
}
else
{
Row newParentItem = item.m_parent;
if( newParentItem == null )
newParentItem = this.treeTable.m_rootItem;
int itemPos = item.m_parent.getChilds().indexOf( item );
newParentItem.getChilds().add( itemPos, this );
DOM.insertBefore( this.treeTable.m_body, m_tr, item.m_tr );
}
// take care of the left padding
Element firstTd = DOM.getChild( m_tr, 0 );
firstTd.getStyle().setPaddingLeft( getLevel() * this.treeTable.treePadding, Unit.PX );
// update child rows
Element nextTR = DOM.getNextSibling( m_tr );
if( firstChildRow != null && lastTrToMove != null && hasChilds() )
{
while( true )
{
Element next = DOM.getNextSibling( firstChildRow );
DOM.insertBefore( this.treeTable.m_body, firstChildRow, nextTR );
if( firstChildRow == lastTrToMove )
break;
firstChildRow = next;
}
}
}
public Element getTdElement( int column )
{
return DOM.getChild( m_tr, column );
}
public Row getNextTraversalItem()
{
// if has child, return first child
if( hasChilds() )
return m_childs.get( 0 );
// return next sibling if any
Row parent = m_parent;
if( parent == null )
parent = this.treeTable.m_rootItem;
int me = parent.m_childs.indexOf( this );
if( me < parent.m_childs.size() - 1 )
return parent.m_childs.get( me + 1 );
// return the next sibling of our parent
Row parentNext = null;
parent = m_parent;
while( parentNext == null && parent != null )
{
parentNext = parent.getNextSiblingNoBack();
parent = parent.m_parent;
}
if( parentNext != null )
return parentNext;
return this.treeTable.m_rootItem.getChilds().get( 0 );
}
private Row getNextSiblingNoBack()
{
Row parent = m_parent;
if( parent == null )
parent = this.treeTable.m_rootItem;
int me = parent.m_childs.indexOf( this );
if( me == parent.m_childs.size() - 1 )
return null;
return parent.m_childs.get( me + 1 );
}
public Row getPrevTraversalItem()
{
Row parent = m_parent;
if( parent == null )
parent = this.treeTable.m_rootItem;
int me = parent.m_childs.indexOf( this );
if( me == 0 )
return parent.m_childs.get( parent.m_childs.size() - 1 );
return parent.m_childs.get( me - 1 );
}
public Row getNextSiblingItem()
{
Row parent = m_parent;
if( parent == null )
parent = this.treeTable.m_rootItem;
int me = parent.m_childs.indexOf( this );
if( me == parent.m_childs.size() - 1 )
return parent.m_childs.get( 0 );
return parent.m_childs.get( me + 1 );
}
public Row getPrevSiblingItem()
{
Row parent = m_parent;
if( parent == null )
parent = this.treeTable.m_rootItem;
int me = parent.m_childs.indexOf( this );
if( me == 0 )
return parent.m_childs.get( parent.m_childs.size() - 1 );
return parent.m_childs.get( me - 1 );
}
public void setRef( int ref )
{
if( m_ref != ref )
{
m_tr.setAttribute( "ref", String.valueOf( ref ) );
m_ref = ref;
}
}
public int getRef()
{
if( m_ref < 0 )
{
String value = m_tr.getAttribute( "ref" );
if( value == null )
return -1;
try
{
m_ref = Integer.parseInt( value );
return m_ref;
}
catch( Exception e )
{
return -1;
}
}
return m_ref;
}
public void setVisible( final boolean isVisible )
{
visitTreeDeep( new IAsyncCallback<Row>()
{
@Override
public void onSuccess( Row result )
{
if( isVisible )
result.m_tr.getStyle().clearDisplay();
else
result.m_tr.getStyle().setDisplay( Display.NONE );
}
} );
}
public void setDataObject( Object dataObject )
{
m_dataObject = dataObject;
}
public Object getDataObject()
{
return m_dataObject;
}
public void setText( int column, String text )
{
assert column < this.treeTable.m_nbColumns;
if( column >= this.treeTable.m_nbColumns )
return;
if( m_tr == null )
return;
Element td = DOM.getChild( m_tr, column );
this.treeTable.clearCell( td );
if( column == 0 )
{
SafeHtmlBuilder sb = new SafeHtmlBuilder();
sb.append( getExpandImageHtml() );
sb.appendEscaped( text );
td.setInnerHTML( sb.toSafeHtml().asString() );
}
else
{
td.setInnerText( text );
}
}
public void setText( int column, int text )
{
setText( column, String.valueOf( text ) );
}
public void setText( int column, double text )
{
setText( column, String.valueOf( text ) );
}
public void setHTML( int column, String html )
{
assert column < this.treeTable.m_nbColumns;
if( column >= this.treeTable.m_nbColumns )
return;
if( m_tr == null )
return;
Element td = DOM.getChild( m_tr, column );
this.treeTable.clearCell( td );
if( column == 0 )
td.setInnerHTML( getExpandImageHtml().asString() + html );
else
td.setInnerHTML( html );
}
public void setWidget( int column, IsWidget w )
{
setWidget( column, w.asWidget() );
}
public void setWidget( int column, Widget w )
{
assert column < this.treeTable.m_nbColumns;
if( column >= this.treeTable.m_nbColumns )
return;
if( m_tr == null )
return;
Element td = DOM.getChild( m_tr, column );
treeTable.clearCell( td );
if( column == 0 )
td.setInnerHTML( getExpandImageHtml().asString() );
treeTable.addWidget( td, w );
}
public void highLite()
{
JQuery.get().jqEffect( "highlight", 2000, m_tr, null );
}
public void addClassRow( String clazz )
{
m_tr.addClassName( clazz );
}
public void removeClassRow( String clazz )
{
m_tr.removeClassName( clazz );
}
public boolean hasChilds()
{
return (m_childs != null) && (!m_childs.isEmpty());
}
public ArrayList<Row> getChilds()
{
if( m_childs == null )
m_childs = new ArrayList<>();
return m_childs;
}
public boolean getExpanded()
{
return m_fExpanded;
}
public void setExpanded( boolean fExpanded )
{
m_fExpanded = fExpanded;
signalStateChange();
ensureAllChildRespectExpand();
}
void signalStateChange()
{
updateExpandImage();
}
private void visitTreeDeep( IAsyncCallback<Row> callback )
{
if( hasChilds() )
{
for( Row child : getChilds() )
child.visitTreeDeep( callback );
}
callback.onSuccess( this );
}
void ensureAllChildRespectExpand()
{
boolean fOneParentAboveShrinked = false;
Row parent = this;
while( parent != null )
{
if( !parent.m_fExpanded )
{
fOneParentAboveShrinked = true;
break;
}
parent = parent.m_parent;
}
ensureAllChildRespectExpand( fOneParentAboveShrinked );
}
void ensureAllChildRespectExpand( boolean fOneParentAboveShrinked )
{
if( !hasChilds() )
return;
for( Row child : getChilds() )
{
if( m_fExpanded && (!fOneParentAboveShrinked) )
child.m_tr.getStyle().clearDisplay();
else
child.m_tr.getStyle().setDisplay( Display.NONE );
child.ensureAllChildRespectExpand( fOneParentAboveShrinked || !m_fExpanded );
}
}
Row getLastLeaf()
{
if( !hasChilds() )
return this;
int nbChilds = m_childs.size();
return m_childs.get( nbChilds - 1 ).getLastLeaf();
}
Element getLastLeafTR()
{
return getLastLeaf().m_tr;
}
public void remove()
{
removeRec();
// remove from parent
if( m_parent != null )
m_parent.getChilds().remove( this );
}
public void removeRec()
{
// remove all children
while( hasChilds() )
m_childs.remove( 0 ).remove();
// remove all widgets
removeAllWidgets();
// abandon row
setRef( -1 );
m_trToDelete = m_tr;
m_tr = null;
// remove row
if( m_trToDelete != null )
{
final Element tr = m_trToDelete;
m_trToDelete = null;
JQuery.get().jqFadeOut( tr, 250, new JQuery.Callback()
{
@Override
public void onFinished()
{
// physical remove
try
{
Row.this.treeTable.m_body.removeChild( tr );
}
catch( Exception e )
{
}
}
} );
}
}
void logicalRemove()
{
// remove from parent
if( m_parent != null )
{
m_parent.m_childs.remove( this );
m_parent.signalStateChange();
}
m_parent = null;
setRef( -1 );
m_trToDelete = m_tr;
m_tr = null;
//
if( m_childs != null )
{
for( Row child : m_childs )
child.logicalRemove();
}
}
void removeAllWidgets()
{
int nbTd = DOM.getChildCount( m_tr );
for( int i = 0; i < nbTd; i++ )
{
Element td = DOM.getChild( m_tr, i );
this.treeTable.clearCellWidget( td );
}
}
void physicalRemove()
{
// remove all widgets
removeAllWidgets();
// remove myself...
if( m_trToDelete != null )
this.treeTable.m_body.removeChild( m_trToDelete );
// ...and all my children
if( m_childs != null )
{
for( Row child : m_childs )
child.physicalRemove();
}
}
public int getLevel()
{
if( m_parent == null )
return -1;
return 1 + m_parent.getLevel();
}
public class Cell implements Printer
{
private final int column;
private Cell( int col )
{
this.column = col;
}
@Override
public void setText( String text )
{
Row.this.setText( column, text );
}
@Override
public void setHTML( String html )
{
Row.this.setHTML( column, html );
}
@Override
public void setWidget( Widget widget )
{
Row.this.setWidget( column, widget );
}
public Element getTdElement()
{
return DOM.getChild( m_tr, column );
}
public Size getSize()
{
Element td = getTdElement();
return new Size( td.getOffsetWidth(), td.getOffsetHeight() );
}
}
public boolean isDisplayed()
{
String display = m_tr.getStyle().getDisplay();
return display == null || !display.equalsIgnoreCase( "none" );
}
}