Scope

This page describes the use of the TabbedPanel with close button component.
It is an enhancement of the AjaxTabbedPanel from wicket-extensions allowing tabs to be simply closed by a button.

With this little update, we can now close any tab simply by clicking on its close-button. For example, it's exactly the same graphic method of Firefox, with a little button on the right side of a tab (I say right side, but position can, of course, be changed in css).

Codes / .java

First modification, AbstractTab.class

First, we have to modify AbstractTab class to introduce two ideas:

  1. Tab we're creating will be closeable or not.
  2. A title to identify tabs when we closed them and put view on the good tab.
/**
 * @author Jérémy Goussé
 * @date 10/08/2008
 */
public abstract class MyAbstractTab extends AbstractTab
{
	private String title;
	private boolean canBeClosed;

	public MyAbstractTab(IModel iModel, String title, boolean canBeClosed)
	{
		super(iModel);
		this.title = title;
		this.canBeClosed = canBeClosed;
	}

	public String getOngletTitle()
	{
		return this.title;
	}
	
	public boolean isCanBeClosed()
	{
		return canBeClosed;
	}
}

This class simply extends wicket.extensions.markup.html.tabs.AbstractTab.
We add two variables and so, two methods.
*Variable title and method to get it.
*Variable canBeClosed and method to know if this tab is closeable or not. (true means closeable, false will hide the close button)

Second modification, our TabbedPanel

.java

So we created tabs, no we have to create a panel who contains our tabs.
I simply rewrited org.apache.wicket.extensions.markup.html.tabs.TabbedPanel and add some modifications.
Let see the code, I'll explain just after my modifications.

/**
 * 
 * @author Jérémy Goussé
 * 
 */
public class MyTabbedPanel extends Panel
{
	public static final String	TAB_PANEL_ID	= "panel";
	private String	whereAmI;
	private final List<MyAbstractTab>	tabs;

	public MyTabbedPanel( String id, List<MyAbstractTab> tabs )
	{
		super( id, new Model( new Integer( -1 ) ) );

		this.setOutputMarkupId( true );

		if ( tabs == null ) { throw new IllegalArgumentException( "argument [tabs] cannot be null" ); }

		this.tabs = tabs;

		final IModel tabCount = new AbstractReadOnlyModel()
		{
			private static final long	serialVersionUID	= 1L;

			public Object getObject()
			{
				return new Integer( MyTabbedPanel.this.tabs.size() );
			}
		};

		WebMarkupContainer tabsContainer = new WebMarkupContainer( "tabs-container" )
		{
			private static final long	serialVersionUID	= 1L;

			protected void onComponentTag( ComponentTag tag )
			{
				super.onComponentTag( tag );
				tag.put( "class", getTabContainerCssClass() );
			}
		};
		add( tabsContainer );

		// add the loop used to generate tab names
		tabsContainer.add( new Loop( "tabs", tabCount )
		{
			private static final long	serialVersionUID	= 1L;

			protected void populateItem( LoopItem item )
			{
				final int index = item.getIteration();

				final WebMarkupContainer titleLink = newLink( "link", index );

				titleLink.add( newTitle( "title", MyTabbedPanel.this.tabs.get( index ).getOngletTitle(), index ) );
				item.add( titleLink );

				Form form = new Form( "tabForm" );

				AjaxButton closeButton = new AjaxButton( "closeTab", form )
				{
					@Override
					protected void onSubmit( AjaxRequestTarget target, Form form )
					{
						removeTab( target, index );
					}
				};

				if ( !MyTabbedPanel.this.tabs.get( index ).isCanBeClosed() )
					closeButton.setVisible( false );

				form.add( closeButton );
				item.add( form );
			}

			protected LoopItem newItem( int iteration )
			{
				return newTabContainer( iteration );
			}

		} );

		this.whereAmI = this.tabs.get( 0 ).getOngletTitle();
	}

	protected LoopItem newTabContainer( int tabIndex )
	{
		return new LoopItem( tabIndex )
		{
			private static final long	serialVersionUID	= 1L;

			protected void onComponentTag( ComponentTag tag )
			{
				super.onComponentTag( tag );
				String cssClass = (String)tag.getString( "class" );
				if ( cssClass == null )
				{
					cssClass = " ";
				}
				cssClass += " tab" + getIteration();

				if ( getIteration() == getSelectedTab() )
				{
					cssClass += " selected";
				}
				if ( getIteration() == getTabs().size() - 1 )
				{
					cssClass += " last";
				}
				tag.put( "class", cssClass.trim() );
			}

		};
	}

	protected void onBeforeRender()
	{
		super.onBeforeRender();
		if ( !hasBeenRendered() && getSelectedTab() == -1 )
		{
			// select the first tab by default
			setSelectedTab( 0 );
		}
	}

	protected String getTabContainerCssClass()
	{
		return "tab-row";
	}

	public final List<*MyAbstractTab*> getTabs()
	{
		return tabs;
	}

	protected Component newTitle( String titleId, String title, int index )
	{
		return new Label( titleId, title );
	}

	public void removeTab( AjaxRequestTarget target, int index )
	{
		String titleDeletedTab = this.tabs.get( index ).getOngletTitle();
		this.tabs.remove( index );

		if ( titleDeletedTab.equals( this.whereAmI ) )
		{
			this.setSelectedTab( 0 );
			this.whereAmI = this.tabs.get( 0 ).getOngletTitle();
		}
		else
		{
			this.setSelectedTab( this.findSelectedTab( whereAmI ) );
		}

		target.addComponent( this );
	}

	protected int findSelectedTab( String title )
	{
		int result = 0;
		for ( int i = 0 ; i < this.tabs.size() ; i++ )
			if ( title.equals( this.tabs.get( i ).getOngletTitle() ) )
				result = i;
		return result;
	}

	protected WebMarkupContainer newLink( String linkId, final int index )
	{
		return new Link( linkId )
		{
			public void onClick()
			{
				setSelectedTab( index );
			}
		};
	}

	public final void setSelectedTab( int index )
	{
		if ( index < 0 || index >= tabs.size() ) { throw new IndexOutOfBoundsException(); }

		setModelObject( new Integer( index ) );

		ITab tab = (ITab)tabs.get( index );

		Panel panel = tab.getPanel( TAB_PANEL_ID );

		if ( panel == null ) { throw new WicketRuntimeException( "ITab.getPanel() returned null. TabbedPanel [" + getPath()
				+ "] ITab index [" + index + "]" );

		}

		if ( !panel.getId().equals( TAB_PANEL_ID ) ) { throw new WicketRuntimeException(
				"ITab.getPanel() returned a panel with invalid id [" + panel.getId()
						+ "]. You must always return a panel with id equal to the provided panelId parameter. TabbedPanel [" + getPath()
						+ "] ITab index [" + index + "]" ); }

		if ( get( TAB_PANEL_ID ) == null )
		{
			add( panel );
		}
		else
		{
			replace( panel );
		}

		this.whereAmI = this.tabs.get( index ).getOngletTitle();
	}

	public final int getSelectedTab()
	{
		return ( (Integer)getModelObject() ).intValue();
	}

Let's see and describe my modifications:

  1. First, don't forget to changed all old AbstractTab by our new class, MyAbsctractTab anywhere in this code
  2. A new variable is add: "whereAmI". Thanks to it, we could know at any time where we are, or what we're whatching. So when we created our TabbedPanel we specify we're looking first tab.
    When we changed selected tab by setSelectTab(int index) we changed whereAmI too.
    And if we remove tab we're whatching, we set view on the first tab. (in my code my first tab always exists cause i put variable canBeClosed in MyAbstractTab on false value).
  3. I add "this.setOutputMarkupId( true )" too in MyTabbedPanel constructor to let Ajax update it.
  4. Most important parts are the new button and the removeTab function.
    In constructor, I add the new button, override his onSubmit method to call removeTab function. I created a form for this button, and put the button on visible or not (varibale canBeClosed).
    RemoveTab function just remove tab corresponding to button you pushed, and call setSelectabTab to be sure of view context.
  5. I created a new function, findSelectedTab, which has the same goal of geSelectTab function, but do it with title of the tab.
    Thanks to this new function, it's simplest to know position of any tab, simply by giving name.

.html

To finish, here the corresponding html code of class above.

<wicket:panel>
<div wicket:id="tabs-container" class="tab-row">
<ul>
	<li wicket:id="tabs">
		<div class="tab"><a href="#" wicket:id="link"><span wicket:id="title">[[tab title]]</span></a><form wicket:id="tabForm"><input type="button" wicket:id="closeTab"/></form></div>
	</li>
</ul>
</div>
<div wicket:id="panel" class="tab-panel">[panel]</div>
</wicket:panel>

With a correct CssStylesheet you can easily transform your tab.