Thursday, November 15, 2007

Suckerfish dropdowns for Wicket

Note: there is an updated post on this topic.

We wanted to add Suckerfish dropdowns to our Wicket site. Suckerfish dropdowns were first mentioned on A List Apart and have since become very popular as super-simple, lightweight, easy-to-use Javascript menus. The licensing is liberal too, see the "Free source" section on the Copyright page. Since then they have been refined as "Son of Suckerfish".

As is common with Wicket, this turned out to be easy to do. All we need was single-level menus (i.e. no sub-submenus), but it should be straightforward to extend this to support that. I also wanted to keep it as simple as possible for illustration purposes. The solution consists of just one class.

Note: this has only been tested with Wicket 1.3.0.

First an example of how to use it.

MyWebPage.java

...
// Create the menubar
final SuckerfishMenuPanel mb = new SuckerfishMenuPanel("menuBar");
add(mb);
// Create a menu
SuckerfishMenuPanel.MenuItem mi =
new SuckerfishMenuPanel.MenuItem(new
BookmarkablePageLink(SuckerfishMenuPanel.LINK_ID, MyFirstPage.class),
"myFirstLabel");
mb.addMenu(mi);
// Create a submenu
SuckerfishMenuPanel.MenuItem submi =
new SuckerfishMenuPanel.MenuItem(new
BookmarkablePageLink(SuckerfishMenuPanel.LINK_ID, MySecondPage.class),
"mySecondLabel");
mi.addMenu(submi);
...

MyWebPage.html

...
<div wicket:id="menuBar" class="menubar">[MenuBar Here]</div>
...

Now the implementation. Note that these menus consist of Links. This gives you the freedom to pass in whatever type of Link you need, except for ExternalLink, which unfortunately does not inherit from Link.

SuckerfishMenuPanel.java

/**
* See http://www.alistapart.com/articles/dropdowns/
* License is at
* http://www.alistapart.com/copyright/, see "Free source"
* Currently only supports one level of dropdowns
*/
public class SuckerfishMenuPanel extends Panel
{
private static final long serialVersionUID = -21832859336423477L;

public static final String LINK_ID = "linkid";
public static final String LINK_TEXT_ID = "linktext";
private final List topMenuItems = new ArrayList();

public SuckerfishMenuPanel(String id)
{
super(id);
// Add the Suckerfish CSS
add(HeaderContributor.forCss(SuckerfishMenuPanel.class,
"SuckerfishMenuPanel.css"));
// Add the top menus
add(new ListView("menubarlinks", new PropertyModel(this, "topMenuItems"))
{
private static final long serialVersionUID = -5875124377225299067L;

@Override
protected void populateItem(ListItem item)
{
final MenuItem menuItem = (MenuItem) item.getModelObject();
item.add(menuItem.getLink());
final WebMarkupContainer menuitemul = new WebMarkupContainer("menuitemul");
item.add(menuitemul);
// Hide the ul tag if there are no submenus
menuitemul.setVisible(menuItem.getChildren().size() > 0);
// Add the submenus
final ListView menuitemlinks =
new ListView("menuitemlinks", menuItem.getChildren())
{
private static final long serialVersionUID = -2997784619579088676L;

@Override
protected void populateItem(ListItem item)
{
final MenuItem menuItem = (MenuItem) item
.getModelObject();
item.add(menuItem.getLink());
}
};
menuitemul.add(menuitemlinks);
}
});
}
/** Add one menu item */
public void addMenu(MenuItem menu)
{
if (!menu.link.getId().equals(LINK_ID))
{
throw new IllegalArgumentException(
"The id must be SuckerfishMenuPanel.LINK_ID");
}
topMenuItems.add(menu);
}
/** Add all menus at once */
public void setMenuItems(List menuItems)
{
this.topMenuItems.clear();
this.topMenuItems.addAll(menuItems);
}
/** Lightweight menu object that stores a menu Link and its label */
public static class MenuItem
{
private final Link link;
private final List subMenuItems = new ArrayList();

public MenuItem(Link link, String label)
{
this.link = link;
this.link.add(new Label(LINK_TEXT_ID, label));
}
/** Add one menu item */
public void addMenu(MenuItem menu)
{
if (!menu.link.getId().equals(LINK_ID))
{
throw new IllegalArgumentException(
"The id must be SuckerfishMenuPanel.LINK_ID");
}
subMenuItems.add(menu);
}
/** Add all menus at once */
public void setMenuItems(List menuItems)
{
this.subMenuItems.clear();
for (MenuItem child : menuItems)
{
addMenu(child);
}
}

public Link getLink()
{
return link;
}

public List getChildren()
{
return subMenuItems;
}
}
}

SuckerfishMenuPanel.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:wicket="http://wicket.sourceforge.net/" xml:lang="en" lang="en">
<wicket:head>
<script language="JavaScript" type="text/javascript">
myHover = function() {
var sfEls = document.getElementById("nav").getElementsByTagName("LI");
for (var i=0; i<sfEls.length; i++) {
sfEls[i].onmouseover=function() {
this.className+=" myhover";
}
sfEls[i].onmouseout=function() {
this.className=this.className.replace(new RegExp(" myhover\\b"), "");
}
}
}
if (window.attachEvent) window.attachEvent("onload", myHover);
</script>
</wicket:head>
<body>
<wicket:panel>
<ul id="nav">
<li wicket:id="menubarlinks">
<a href="#" wicket:id="linkid"
class="topmenu"><span wicket:id="linktext">Link Text</span></a>
<ul wicket:id="menuitemul">
<li wicket:id="menuitemlinks">
<a href="#" wicket:id="linkid"
class="childmenu"><span wicket:id="linktext">Link Text</span></a>
</li>
</ul>
</li>
</ul>
</wicket:panel>
</body>
</html>

The CSS below is for horizontal menus. It can be changed to accommodate vertical menus. I'm using a background image (gradient.gif). You will either need to supply your own or just use a plain background.

SuckerfishMenuPanel.css

#nav, #nav ul {
float: left;
width: 99.9%;
list-style: none;
line-height: 1;
font-weight: bold;
padding: 0;
margin: 0;
border: 1px solid #3e6581;
background:#BCCDDB
url(/images/gradient.gif)
repeat-x 0% 0%;
}
#nav a {
display: block;
width: 10em;
w\idth: 6em;
color: #7C6240;
text-decoration: none;
padding: 0.25em;
}
#nav a.topmenu {
width: 10em;
text-align: center;
width: auto;
}
#nav a.topmenu span {
color: white;
}
#nav a.childmenu {
width: 13em;
}
#nav a.childmenu span {
color: white;
}
#nav li {
float: left;
padding-left: 0.5em;
padding-right: 0.5em;
padding-top: 0.5em;
padding-bottom: 0.5em;
width: auto;
background:#BCCDDB
url(/images/gradient.gif)
repeat-x 0% 0%;
}
#nav li ul {
position: absolute;
left: -999em;
height: auto;
width: 14.4em;
w\idth: 13.9em;
font-weight: normal;
margin: 0;
}
#nav li li {
padding-right: 1em;
width: 13em
padding-left: 0;
padding-top: 0.25em;
padding-bottom: 0.25em;
}
#nav li ul a {
width: 13em;
w\idth: 9em;
}

#nav li ul ul {
margin: -1.75em 0 0 14em;
}
#nav li:hover ul ul, #nav li:hover ul ul ul, #nav li.myhover ul ul, #nav li.myhover ul ul ul {
left: -999em;
}
#nav li:hover ul, #nav li li:hover ul, #nav li li li:hover ul, #nav li.myhover ul, #nav li li.myhover ul, #nav li li li.myhover ul {
left: auto;
}
#nav li:hover, #nav li.myhover {
background:#C8D7E3
url(/images/gradient-highlighted.gif)
repeat-x;
}

6 comments:

Suresh Byanjankar said...

no get method defined for topMenuItems

Ryan Sonnek said...

this would be a great contribution to the wicket stuff project so others could use it!

Julian Sinai said...

Suresh, I believe the problem was that you were using Wicket 1.2.6, and I didn't test my code with that version. Sorry about that. But I'm sure it's pretty easy to modify.

Ryan, thanks for the suggestion. It's done!

Nili said...

This is exactly what I need. Where can I download this module?

Julian Sinai said...

Nili, see the updated post. The code is available from Wicket Stuff.

CJ said...

I think php tutorial is more comfortable to use unless java.