/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.wicket.extensions.markup.html.tabs; import java.util.List; import org.apache.wicket.Component; import org.apache.wicket.WicketRuntimeException; import org.apache.wicket.core.util.string.CssUtils; import org.apache.wicket.markup.ComponentTag; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.link.Link; import org.apache.wicket.markup.html.list.Loop; import org.apache.wicket.markup.html.list.LoopItem; import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; import org.apache.wicket.util.lang.Args; /** * TabbedPanel component represents a panel with tabs that are used to switch between different * content panels inside the TabbedPanel panel. * <p> * <b>Note:</b> When the currently selected tab is replaced by changing the underlying list of tabs, * the change is not picked up unless a call is made to {@link #setSelectedTab(int)}. * <p> * Example: * * <pre> * List tabs=new ArrayList(); * tabs.add(new AbstractTab(new Model<String>("first tab")) { * public Panel getPanel(String panelId) * { * return new TabPanel1(panelId); * } * }); * * tabs.add(new AbstractTab(new Model<String>("second tab")) { * public Panel getPanel(String panelId) * { * return new TabPanel2(panelId); * } * }); * * add(new TabbedPanel("tabs", tabs)); * * <span wicket:id="tabs" class="tabpanel">[tabbed panel will be here]</span> * </pre> * <p> * For a complete example see the component references in wicket-examples project * * @see org.apache.wicket.extensions.markup.html.tabs.ITab * * @author Igor Vaynberg (ivaynberg at apache dot org) * @param <T> * The type of panel to be used for this component's tabs. Just use {@link ITab} if you * have no special needs here. */ public class TabbedPanel<T extends ITab> extends Panel { private static final long serialVersionUID = 1L; /** id used for child panels */ public static final String TAB_PANEL_ID = "panel"; public static final String CONTAINER_CSS_CLASS_KEY = CssUtils.key(TabbedPanel.class, "container"); public static final String SELECTED_CSS_CLASS_KEY = CssUtils.key(TabbedPanel.class, "selected"); public static final String LAST_CSS_CLASS_KEY = CssUtils.key(TabbedPanel.class, "last"); private final List<T> tabs; /** the current tab */ private int currentTab = -1; private transient VisibilityCache visibilityCache; /** * Constructor * * @param id * component id * @param tabs * list of ITab objects used to represent tabs */ public TabbedPanel(final String id, final List<T> tabs) { this(id, tabs, null); } /** * Constructor * * @param id * component id * @param tabs * list of ITab objects used to represent tabs * @param model * model holding the index of the selected tab */ public TabbedPanel(final String id, final List<T> tabs, IModel<Integer> model) { super(id, model); this.tabs = Args.notNull(tabs, "tabs"); final IModel<Integer> tabCount = new IModel<Integer>() { private static final long serialVersionUID = 1L; @Override public Integer getObject() { return TabbedPanel.this.tabs.size(); } }; WebMarkupContainer tabsContainer = newTabsContainer("tabs-container"); add(tabsContainer); // add the loop used to generate tab names tabsContainer.add(new Loop("tabs", tabCount) { private static final long serialVersionUID = 1L; @Override protected void populateItem(final LoopItem item) { final int index = item.getIndex(); final T tab = TabbedPanel.this.tabs.get(index); final WebMarkupContainer titleLink = newLink("link", index); titleLink.add(newTitle("title", tab.getTitle(), index)); item.add(titleLink); } @Override protected LoopItem newItem(final int iteration) { return newTabContainer(iteration); } }); add(newPanel()); } /** * Override of the default initModel behaviour. This component <strong>will not</strong> use any * compound model of a parent. * * @see org.apache.wicket.Component#initModel() */ @Override protected IModel<?> initModel() { return new Model<>(-1); } /** * Generates the container for all tabs. The default container automatically adds the css * <code>class</code> attribute based on the return value of {@link #getTabContainerCssClass()} * * @param id * container id * @return container */ protected WebMarkupContainer newTabsContainer(final String id) { return new WebMarkupContainer(id) { private static final long serialVersionUID = 1L; @Override protected void onComponentTag(final ComponentTag tag) { super.onComponentTag(tag); tag.put("class", getTabContainerCssClass()); } }; } /** * Generates a loop item used to represent a specific tab's <code>li</code> element. * * @param tabIndex * @return new loop item */ protected LoopItem newTabContainer(final int tabIndex) { return new LoopItem(tabIndex) { private static final long serialVersionUID = 1L; @Override protected void onConfigure() { super.onConfigure(); setVisible(getVisiblityCache().isVisible(tabIndex)); } @Override protected void onComponentTag(final ComponentTag tag) { super.onComponentTag(tag); String cssClass = tag.getAttribute("class"); if (cssClass == null) { cssClass = " "; } cssClass += " tab" + getIndex(); if (getIndex() == getSelectedTab()) { cssClass += ' ' + getSelectedTabCssClass(); } if (getVisiblityCache().getLastVisible() == getIndex()) { cssClass += ' ' + getLastTabCssClass(); } tag.put("class", cssClass.trim()); } }; } @Override protected void onBeforeRender() { int index = getSelectedTab(); if ((index == -1) || (getVisiblityCache().isVisible(index) == false)) { // find first visible tab index = -1; for (int i = 0; i < tabs.size(); i++) { if (getVisiblityCache().isVisible(i)) { index = i; break; } } if (index != -1) { // found a visible tab, so select it setSelectedTab(index); } } setCurrentTab(index); super.onBeforeRender(); } /** * @return the value of css class attribute that will be added to a div containing the tabs. The * default value is <code>tab-row</code> */ protected String getTabContainerCssClass() { return getString(CONTAINER_CSS_CLASS_KEY); } /** * @return the value of css class attribute that will be added to last tab. The default value is * <code>last</code> */ protected String getLastTabCssClass() { return getString(LAST_CSS_CLASS_KEY); } /** * @return the value of css class attribute that will be added to selected tab. The default * value is <code>selected</code> */ protected String getSelectedTabCssClass() { return getString(SELECTED_CSS_CLASS_KEY); } /** * @return list of tabs that can be used by the user to add/remove/reorder tabs in the panel */ public final List<T> getTabs() { return tabs; } /** * Factory method for tab titles. Returned component can be anything that can attach to span * tags such as a fragment, panel, or a label * * @param titleId * id of title component * @param titleModel * model containing tab title * @param index * index of tab * @return title component */ protected Component newTitle(final String titleId, final IModel<?> titleModel, final int index) { return new Label(titleId, titleModel); } /** * Factory method for links used to switch between tabs. * * The created component is attached to the following markup. Label component with id: title * will be added for you by the tabbed panel. * * <pre> * <a href="#" wicket:id="link"><span wicket:id="title">[[tab title]]</span></a> * </pre> * * Example implementation: * * <pre> * protected WebMarkupContainer newLink(String linkId, final int index) * { * return new Link(linkId) * { * private static final long serialVersionUID = 1L; * * public void onClick() * { * setSelectedTab(index); * } * }; * } * </pre> * * @param linkId * component id with which the link should be created * @param index * index of the tab that should be activated when this link is clicked. See * {@link #setSelectedTab(int)}. * @return created link component */ protected WebMarkupContainer newLink(final String linkId, final int index) { return new Link<Void>(linkId) { private static final long serialVersionUID = 1L; @Override public void onClick() { setSelectedTab(index); } }; } /** * sets the selected tab * * @param index * index of the tab to select * @return this for chaining * @throws IndexOutOfBoundsException * if index is not in the range of available tabs */ public TabbedPanel<T> setSelectedTab(final int index) { if ((index < 0) || (index >= tabs.size())) { throw new IndexOutOfBoundsException(); } setDefaultModelObject(index); // force the tab's component to be aquired again if already the current tab currentTab = -1; setCurrentTab(index); return this; } private void setCurrentTab(int index) { if (this.currentTab == index) { // already current return; } this.currentTab = index; final Component component; if (currentTab == -1 || (tabs.size() == 0) || !getVisiblityCache().isVisible(currentTab)) { // no tabs or the current tab is not visible component = newPanel(); } else { // show panel from selected tab T tab = tabs.get(currentTab); component = tab.getPanel(TAB_PANEL_ID); if (component == null) { throw new WicketRuntimeException("ITab.getPanel() returned null. TabbedPanel [" + getPath() + "] ITab index [" + currentTab + "]"); } } if (!component.getId().equals(TAB_PANEL_ID)) { throw new WicketRuntimeException( "ITab.getPanel() returned a panel with invalid id [" + component.getId() + "]. You must always return a panel with id equal to the provided panelId parameter. TabbedPanel [" + getPath() + "] ITab index [" + currentTab + "]"); } addOrReplace(component); } private WebMarkupContainer newPanel() { return new WebMarkupContainer(TAB_PANEL_ID); } /** * @return index of the selected tab */ public final int getSelectedTab() { return (Integer)getDefaultModelObject(); } @Override protected void onDetach() { visibilityCache = null; super.onDetach(); } private VisibilityCache getVisiblityCache() { if (visibilityCache == null) { visibilityCache = new VisibilityCache(); } return visibilityCache; } /** * A cache for visibilities of {@link ITab}s. */ private class VisibilityCache { /** * Visibility for each tab. */ private Boolean[] visibilities; /** * Last visible tab. */ private int lastVisible = -1; public VisibilityCache() { visibilities = new Boolean[tabs.size()]; } public int getLastVisible() { if (lastVisible == -1) { for (int t = 0; t < tabs.size(); t++) { if (isVisible(t)) { lastVisible = t; } } } return lastVisible; } public boolean isVisible(int index) { if (visibilities.length < index + 1) { Boolean[] resized = new Boolean[index + 1]; System.arraycopy(visibilities, 0, resized, 0, visibilities.length); visibilities = resized; } if (visibilities.length > 0) { Boolean visible = visibilities[index]; if (visible == null) { visible = tabs.get(index).isVisible(); visibilities[index] = visible; } return visible; } else { return false; } } } }