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.markup.html.tabs; 018 019import java.util.List; 020 021import org.apache.wicket.Component; 022import org.apache.wicket.WicketRuntimeException; 023import org.apache.wicket.core.util.string.CssUtils; 024import org.apache.wicket.markup.ComponentTag; 025import org.apache.wicket.markup.html.WebMarkupContainer; 026import org.apache.wicket.markup.html.basic.Label; 027import org.apache.wicket.markup.html.link.Link; 028import org.apache.wicket.markup.html.list.Loop; 029import org.apache.wicket.markup.html.list.LoopItem; 030import org.apache.wicket.markup.html.panel.Panel; 031import org.apache.wicket.model.IModel; 032import org.apache.wicket.model.Model; 033import org.apache.wicket.util.lang.Args; 034 035 036/** 037 * TabbedPanel component represents a panel with tabs that are used to switch between different 038 * content panels inside the TabbedPanel panel. 039 * <p> 040 * <b>Note:</b> When the currently selected tab is replaced by changing the underlying list of tabs, 041 * the change is not picked up unless a call is made to {@link #setSelectedTab(int)}. 042 * <p> 043 * Example: 044 * 045 * <pre> 046 * List tabs=new ArrayList(); 047 * tabs.add(new AbstractTab(new Model<String>("first tab")) { 048 * public Panel getPanel(String panelId) 049 * { 050 * return new TabPanel1(panelId); 051 * } 052 * }); 053 * 054 * tabs.add(new AbstractTab(new Model<String>("second tab")) { 055 * public Panel getPanel(String panelId) 056 * { 057 * return new TabPanel2(panelId); 058 * } 059 * }); 060 * 061 * add(new TabbedPanel("tabs", tabs)); 062 * 063 * <span wicket:id="tabs" class="tabpanel">[tabbed panel will be here]</span> 064 * </pre> 065 * <p> 066 * For a complete example see the component references in wicket-examples project 067 * 068 * @see org.apache.wicket.extensions.markup.html.tabs.ITab 069 * 070 * @author Igor Vaynberg (ivaynberg at apache dot org) 071 * @param <T> 072 * The type of panel to be used for this component's tabs. Just use {@link ITab} if you 073 * have no special needs here. 074 */ 075public class TabbedPanel<T extends ITab> extends Panel 076{ 077 private static final long serialVersionUID = 1L; 078 079 /** id used for child panels */ 080 public static final String TAB_PANEL_ID = "panel"; 081 082 public static final String CONTAINER_CSS_CLASS_KEY = CssUtils.key(TabbedPanel.class, 083 "container"); 084 085 public static final String SELECTED_CSS_CLASS_KEY = CssUtils.key(TabbedPanel.class, "selected"); 086 087 public static final String LAST_CSS_CLASS_KEY = CssUtils.key(TabbedPanel.class, "last"); 088 089 private final List<T> tabs; 090 091 /** the current tab */ 092 private int currentTab = -1; 093 094 private transient VisibilityCache visibilityCache; 095 096 /** 097 * Constructor 098 * 099 * @param id 100 * component id 101 * @param tabs 102 * list of ITab objects used to represent tabs 103 */ 104 public TabbedPanel(final String id, final List<T> tabs) 105 { 106 this(id, tabs, null); 107 } 108 109 /** 110 * Constructor 111 * 112 * @param id 113 * component id 114 * @param tabs 115 * list of ITab objects used to represent tabs 116 * @param model 117 * model holding the index of the selected tab 118 */ 119 public TabbedPanel(final String id, final List<T> tabs, IModel<Integer> model) 120 { 121 super(id, model); 122 123 this.tabs = Args.notNull(tabs, "tabs"); 124 125 final IModel<Integer> tabCount = new IModel<Integer>() 126 { 127 private static final long serialVersionUID = 1L; 128 129 @Override 130 public Integer getObject() 131 { 132 return TabbedPanel.this.tabs.size(); 133 } 134 }; 135 136 WebMarkupContainer tabsContainer = newTabsContainer("tabs-container"); 137 add(tabsContainer); 138 139 // add the loop used to generate tab names 140 tabsContainer.add(new Loop("tabs", tabCount) 141 { 142 private static final long serialVersionUID = 1L; 143 144 @Override 145 protected void populateItem(final LoopItem item) 146 { 147 final int index = item.getIndex(); 148 final T tab = TabbedPanel.this.tabs.get(index); 149 150 final WebMarkupContainer titleLink = newLink("link", index); 151 152 titleLink.add(newTitle("title", tab.getTitle(), index)); 153 item.add(titleLink); 154 } 155 156 @Override 157 protected LoopItem newItem(final int iteration) 158 { 159 return newTabContainer(iteration); 160 } 161 }); 162 163 add(newPanel()); 164 } 165 166 /** 167 * Override of the default initModel behaviour. This component <strong>will not</strong> use any 168 * compound model of a parent. 169 * 170 * @see org.apache.wicket.Component#initModel() 171 */ 172 @Override 173 protected IModel<?> initModel() 174 { 175 return new Model<>(-1); 176 } 177 178 /** 179 * Generates the container for all tabs. The default container automatically adds the css 180 * <code>class</code> attribute based on the return value of {@link #getTabContainerCssClass()} 181 * 182 * @param id 183 * container id 184 * @return container 185 */ 186 protected WebMarkupContainer newTabsContainer(final String id) 187 { 188 return new WebMarkupContainer(id) 189 { 190 private static final long serialVersionUID = 1L; 191 192 @Override 193 protected void onComponentTag(final ComponentTag tag) 194 { 195 super.onComponentTag(tag); 196 tag.put("class", getTabContainerCssClass()); 197 } 198 }; 199 } 200 201 /** 202 * Generates a loop item used to represent a specific tab's <code>li</code> element. 203 * 204 * @param tabIndex 205 * @return new loop item 206 */ 207 protected LoopItem newTabContainer(final int tabIndex) 208 { 209 return new LoopItem(tabIndex) 210 { 211 private static final long serialVersionUID = 1L; 212 213 @Override 214 protected void onConfigure() 215 { 216 super.onConfigure(); 217 218 setVisible(getVisiblityCache().isVisible(tabIndex)); 219 } 220 221 @Override 222 protected void onComponentTag(final ComponentTag tag) 223 { 224 super.onComponentTag(tag); 225 226 String cssClass = tag.getAttribute("class"); 227 if (cssClass == null) 228 { 229 cssClass = " "; 230 } 231 cssClass += " tab" + getIndex(); 232 233 if (getIndex() == getSelectedTab()) 234 { 235 cssClass += ' ' + getSelectedTabCssClass(); 236 } 237 if (getVisiblityCache().getLastVisible() == getIndex()) 238 { 239 cssClass += ' ' + getLastTabCssClass(); 240 } 241 tag.put("class", cssClass.trim()); 242 } 243 }; 244 } 245 246 @Override 247 protected void onBeforeRender() 248 { 249 int index = getSelectedTab(); 250 251 if ((index == -1) || (getVisiblityCache().isVisible(index) == false)) 252 { 253 // find first visible tab 254 index = -1; 255 for (int i = 0; i < tabs.size(); i++) 256 { 257 if (getVisiblityCache().isVisible(i)) 258 { 259 index = i; 260 break; 261 } 262 } 263 264 if (index != -1) 265 { 266 // found a visible tab, so select it 267 setSelectedTab(index); 268 } 269 } 270 271 setCurrentTab(index); 272 273 super.onBeforeRender(); 274 } 275 276 /** 277 * @return the value of css class attribute that will be added to a div containing the tabs. The 278 * default value is <code>tab-row</code> 279 */ 280 protected String getTabContainerCssClass() 281 { 282 return getString(CONTAINER_CSS_CLASS_KEY); 283 } 284 285 /** 286 * @return the value of css class attribute that will be added to last tab. The default value is 287 * <code>last</code> 288 */ 289 protected String getLastTabCssClass() 290 { 291 return getString(LAST_CSS_CLASS_KEY); 292 } 293 294 /** 295 * @return the value of css class attribute that will be added to selected tab. The default 296 * value is <code>selected</code> 297 */ 298 protected String getSelectedTabCssClass() 299 { 300 return getString(SELECTED_CSS_CLASS_KEY); 301 } 302 303 /** 304 * @return list of tabs that can be used by the user to add/remove/reorder tabs in the panel 305 */ 306 public final List<T> getTabs() 307 { 308 return tabs; 309 } 310 311 /** 312 * Factory method for tab titles. Returned component can be anything that can attach to span 313 * tags such as a fragment, panel, or a label 314 * 315 * @param titleId 316 * id of title component 317 * @param titleModel 318 * model containing tab title 319 * @param index 320 * index of tab 321 * @return title component 322 */ 323 protected Component newTitle(final String titleId, final IModel<?> titleModel, final int index) 324 { 325 return new Label(titleId, titleModel); 326 } 327 328 /** 329 * Factory method for links used to switch between tabs. 330 * 331 * The created component is attached to the following markup. Label component with id: title 332 * will be added for you by the tabbed panel. 333 * 334 * <pre> 335 * <a href="#" wicket:id="link"><span wicket:id="title">[[tab title]]</span></a> 336 * </pre> 337 * 338 * Example implementation: 339 * 340 * <pre> 341 * protected WebMarkupContainer newLink(String linkId, final int index) 342 * { 343 * return new Link(linkId) 344 * { 345 * private static final long serialVersionUID = 1L; 346 * 347 * public void onClick() 348 * { 349 * setSelectedTab(index); 350 * } 351 * }; 352 * } 353 * </pre> 354 * 355 * @param linkId 356 * component id with which the link should be created 357 * @param index 358 * index of the tab that should be activated when this link is clicked. See 359 * {@link #setSelectedTab(int)}. 360 * @return created link component 361 */ 362 protected WebMarkupContainer newLink(final String linkId, final int index) 363 { 364 return new Link<Void>(linkId) 365 { 366 private static final long serialVersionUID = 1L; 367 368 @Override 369 public void onClick() 370 { 371 setSelectedTab(index); 372 } 373 }; 374 } 375 376 /** 377 * sets the selected tab 378 * 379 * @param index 380 * index of the tab to select 381 * @return this for chaining 382 * @throws IndexOutOfBoundsException 383 * if index is not in the range of available tabs 384 */ 385 public TabbedPanel<T> setSelectedTab(final int index) 386 { 387 if ((index < 0) || (index >= tabs.size())) 388 { 389 throw new IndexOutOfBoundsException(); 390 } 391 392 setDefaultModelObject(index); 393 394 // force the tab's component to be aquired again if already the current tab 395 currentTab = -1; 396 setCurrentTab(index); 397 398 return this; 399 } 400 401 private void setCurrentTab(int index) 402 { 403 if (this.currentTab == index) 404 { 405 // already current 406 return; 407 } 408 this.currentTab = index; 409 410 final Component component; 411 412 if (currentTab == -1 || (tabs.size() == 0) || !getVisiblityCache().isVisible(currentTab)) 413 { 414 // no tabs or the current tab is not visible 415 component = newPanel(); 416 } 417 else 418 { 419 // show panel from selected tab 420 T tab = tabs.get(currentTab); 421 component = tab.getPanel(TAB_PANEL_ID); 422 if (component == null) 423 { 424 throw new WicketRuntimeException("ITab.getPanel() returned null. TabbedPanel [" + 425 getPath() + "] ITab index [" + currentTab + "]"); 426 } 427 } 428 429 if (!component.getId().equals(TAB_PANEL_ID)) 430 { 431 throw new WicketRuntimeException( 432 "ITab.getPanel() returned a panel with invalid id [" + 433 component.getId() + 434 "]. You must always return a panel with id equal to the provided panelId parameter. TabbedPanel [" + 435 getPath() + "] ITab index [" + currentTab + "]"); 436 } 437 438 addOrReplace(component); 439 } 440 441 private WebMarkupContainer newPanel() 442 { 443 return new WebMarkupContainer(TAB_PANEL_ID); 444 } 445 446 /** 447 * @return index of the selected tab 448 */ 449 public final int getSelectedTab() 450 { 451 return (Integer)getDefaultModelObject(); 452 } 453 454 @Override 455 protected void onDetach() 456 { 457 visibilityCache = null; 458 459 super.onDetach(); 460 } 461 462 private VisibilityCache getVisiblityCache() 463 { 464 if (visibilityCache == null) 465 { 466 visibilityCache = new VisibilityCache(); 467 } 468 469 return visibilityCache; 470 } 471 472 /** 473 * A cache for visibilities of {@link ITab}s. 474 */ 475 private class VisibilityCache 476 { 477 478 /** 479 * Visibility for each tab. 480 */ 481 private Boolean[] visibilities; 482 483 /** 484 * Last visible tab. 485 */ 486 private int lastVisible = -1; 487 488 public VisibilityCache() 489 { 490 visibilities = new Boolean[tabs.size()]; 491 } 492 493 public int getLastVisible() 494 { 495 if (lastVisible == -1) 496 { 497 for (int t = 0; t < tabs.size(); t++) 498 { 499 if (isVisible(t)) 500 { 501 lastVisible = t; 502 } 503 } 504 } 505 506 return lastVisible; 507 } 508 509 public boolean isVisible(int index) 510 { 511 if (visibilities.length < index + 1) 512 { 513 Boolean[] resized = new Boolean[index + 1]; 514 System.arraycopy(visibilities, 0, resized, 0, visibilities.length); 515 visibilities = resized; 516 } 517 518 if (visibilities.length > 0) 519 { 520 Boolean visible = visibilities[index]; 521 if (visible == null) 522 { 523 visible = tabs.get(index).isVisible(); 524 visibilities[index] = visible; 525 } 526 return visible; 527 } 528 else 529 { 530 return false; 531 } 532 } 533 } 534}