001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.wicket.extensions.breadcrumb; 018 019import java.util.ArrayList; 020import java.util.List; 021 022import org.apache.wicket.Component; 023import org.apache.wicket.markup.html.basic.Label; 024import org.apache.wicket.markup.html.list.ListItem; 025import org.apache.wicket.markup.html.list.ListView; 026import org.apache.wicket.markup.html.panel.Panel; 027import org.apache.wicket.model.IDetachable; 028import org.apache.wicket.model.LoadableDetachableModel; 029 030 031/** 032 * A component that renders bread crumbs. By default, it renders a horizontal list from left to 033 * right (oldest left) with bread crumb links and a ' / ' as a separator, e.g. 034 * 035 * <pre> 036 * first / second / third 037 * </pre> 038 * 039 * <p> 040 * Delegates how the bread crumb model works to {@link DefaultBreadCrumbsModel}. 041 * </p> 042 * <p> 043 * Override and provide your own markup file if you want to work with other elements, e.g. uls 044 * instead of spans. 045 * </p> 046 * 047 * @author Eelco Hillenius 048 */ 049public class BreadCrumbBar extends Panel implements IBreadCrumbModel 050{ 051 /** Default crumb component. */ 052 private static final class BreadCrumbComponent extends Panel 053 { 054 private static final long serialVersionUID = 1L; 055 056 /** 057 * Construct. 058 * 059 * @param id 060 * Component id 061 * @param separatorMarkup 062 * markup used as a separator between breadcrumbs 063 * @param index 064 * The index of the bread crumb 065 * @param breadCrumbModel 066 * The bread crumb model 067 * @param breadCrumbParticipant 068 * The bread crumb 069 * @param enableLink 070 * Whether the link should be enabled 071 */ 072 public BreadCrumbComponent(final String id, final String separatorMarkup, final long index, 073 final IBreadCrumbModel breadCrumbModel, 074 final IBreadCrumbParticipant breadCrumbParticipant, final boolean enableLink) 075 { 076 super(id); 077 add(new Label("sep", (index > 0) ? separatorMarkup : "").setEscapeModelStrings(false) 078 .setRenderBodyOnly(true)); 079 BreadCrumbLink link = new BreadCrumbLink("link", breadCrumbModel) 080 { 081 private static final long serialVersionUID = 1L; 082 083 @Override 084 protected IBreadCrumbParticipant getParticipant(final String componentId) 085 { 086 return breadCrumbParticipant; 087 } 088 }; 089 link.setEnabled(enableLink); 090 add(link); 091 link.add(new Label("label", breadCrumbParticipant.getTitle()).setRenderBodyOnly(true)); 092 } 093 } 094 095 /** 096 * List view for rendering the bread crumbs. 097 */ 098 protected class BreadCrumbsListView extends ListView<IBreadCrumbParticipant> 099 implements 100 IBreadCrumbModelListener 101 { 102 private static final long serialVersionUID = 1L; 103 104 private transient boolean dirty = false; 105 106 private transient int size = 0; 107 108 /** 109 * Construct. 110 * 111 * @param id 112 * Component id 113 */ 114 public BreadCrumbsListView(final String id) 115 { 116 super(id); 117 setReuseItems(false); 118 setDefaultModel(new LoadableDetachableModel<List<IBreadCrumbParticipant>>() 119 { 120 private static final long serialVersionUID = 1L; 121 122 @Override 123 protected List<IBreadCrumbParticipant> load() 124 { 125 // save a copy 126 List<IBreadCrumbParticipant> l = new ArrayList<IBreadCrumbParticipant>( 127 allBreadCrumbParticipants()); 128 size = l.size(); 129 return l; 130 } 131 }); 132 } 133 134 @Override 135 public void breadCrumbActivated(final IBreadCrumbParticipant previousParticipant, 136 final IBreadCrumbParticipant breadCrumbParticipant) 137 { 138 signalModelChange(); 139 } 140 141 @Override 142 public void breadCrumbAdded(final IBreadCrumbParticipant breadCrumbParticipant) 143 { 144 } 145 146 @Override 147 public void breadCrumbRemoved(final IBreadCrumbParticipant breadCrumbParticipant) 148 { 149 } 150 151 /** 152 * Signal model change. 153 */ 154 private void signalModelChange() 155 { 156 // else let the listview recalculate it's children immediately; 157 // it was attached, but it needs to go through that again now 158 // as the signaling component attached after this 159 getDefaultModel().detach(); 160 } 161 162 /** 163 * @see org.apache.wicket.markup.html.list.ListView#onBeforeRender() 164 */ 165 @Override 166 protected void onBeforeRender() 167 { 168 super.onBeforeRender(); 169 if (dirty) 170 { 171 dirty = false; 172 } 173 } 174 175 @Override 176 protected void populateItem(final ListItem<IBreadCrumbParticipant> item) 177 { 178 long index = item.getIndex(); 179 IBreadCrumbParticipant breadCrumbParticipant = (IBreadCrumbParticipant)item.getDefaultModelObject(); 180 item.add(newBreadCrumbComponent("crumb", index, size, breadCrumbParticipant)); 181 } 182 } 183 184 private static final long serialVersionUID = 1L; 185 186 private final IBreadCrumbModel decorated; 187 188 /** 189 * Construct. 190 * 191 * @param id 192 * Component id 193 */ 194 public BreadCrumbBar(final String id) 195 { 196 super(id); 197 decorated = new DefaultBreadCrumbsModel(); 198 BreadCrumbsListView breadCrumbsListView = new BreadCrumbsListView("crumbs"); 199 addListener(breadCrumbsListView); 200 add(breadCrumbsListView); 201 } 202 203 204 @Override 205 public void addListener(final IBreadCrumbModelListener listener) 206 { 207 decorated.addListener(listener); 208 } 209 210 @Override 211 public List<IBreadCrumbParticipant> allBreadCrumbParticipants() 212 { 213 return decorated.allBreadCrumbParticipants(); 214 } 215 216 @Override 217 public IBreadCrumbParticipant getActive() 218 { 219 return decorated.getActive(); 220 } 221 222 @Override 223 public void removeListener(final IBreadCrumbModelListener listener) 224 { 225 decorated.removeListener(listener); 226 } 227 228 @Override 229 public void setActive(final IBreadCrumbParticipant breadCrumbParticipant) 230 { 231 decorated.setActive(breadCrumbParticipant); 232 } 233 234 /** 235 * Gets whether the current bread crumb should be displayed as a link (e.g. for refreshing) or 236 * as a disabled link (effectively just a label). The latter is the default. Override if you 237 * want different behavior. 238 * 239 * @return Whether the current bread crumb should be displayed as a link; this method returns 240 * false 241 */ 242 protected boolean getEnableLinkToCurrent() 243 { 244 return false; 245 } 246 247 /** 248 * @return markup used as a separator between breadcrumbs. By default <code>/</code> is used, 249 * but <code>>></code> is also a popular choice. 250 */ 251 protected String getSeparatorMarkup() 252 { 253 return "/"; 254 } 255 256 /** 257 * Creates a new bread crumb component. That component will be rendered as part of the bread 258 * crumbs list (which is a <ul> <li> structure). 259 * 260 * @param id 261 * The component id 262 * @param index 263 * The index of the bread crumb 264 * @param total 265 * The total number of bread crumbs in the current model 266 * @param breadCrumbParticipant 267 * the bread crumb 268 * @return A new bread crumb component 269 */ 270 protected Component newBreadCrumbComponent(final String id, final long index, final int total, 271 final IBreadCrumbParticipant breadCrumbParticipant) 272 { 273 boolean enableLink = getEnableLinkToCurrent() || (index < (total - 1)); 274 return new BreadCrumbComponent(id, getSeparatorMarkup(), index, this, 275 breadCrumbParticipant, enableLink); 276 } 277 278 @Override 279 protected void onDetach() 280 { 281 super.onDetach(); 282 for (IBreadCrumbParticipant crumb : decorated.allBreadCrumbParticipants()) 283 { 284 if (crumb instanceof Component) 285 { 286 ((Component)crumb).detach(); 287 } 288 else if (crumb instanceof IDetachable) 289 { 290 ((IDetachable)crumb).detach(); 291 } 292 } 293 } 294}