/** * Copyright (C) 2010 Asterios Raptis * * Licensed 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 de.alpharogroup.wicket.components.ajax.editable.tabs; import java.util.List; import org.apache.wicket.Component; import org.apache.wicket.WicketRuntimeException; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.markup.html.AjaxFallbackLink; import org.apache.wicket.behavior.AttributeAppender; import org.apache.wicket.extensions.markup.html.tabs.ITab; 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.list.Loop; import org.apache.wicket.markup.html.list.LoopItem; import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.model.AbstractReadOnlyModel; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; import org.apache.wicket.util.lang.Args; import lombok.Getter; /** * The Class AjaxCloseableTabbedPanel adds functionality to add or remove tabs from the TabbedPanel. * * @param <T> * the generic type */ public class AjaxCloseableTabbedPanel<T extends ICloseableTab> extends Panel { /** * 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; /** * Instantiates a new {@link VisibilityCache}. */ public VisibilityCache() { visibilities = new Boolean[tabs.size()]; } /** * Gets the last visible tab. * * @return the last visible tab. */ public int getLastVisible() { if (lastVisible == -1) { for (int t = 0; t < tabs.size(); t++) { if (isVisible(t)) { lastVisible = t; } } } return lastVisible; } /** * Checks if is visible. * * @param index * the index * @return true, if is visible */ public boolean isVisible(final int index) { if (visibilities.length < (index + 1)) { final 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) { if ((index == 1) && (index == tabs.size())) { visible = tabs.get(0).isVisible(); visibilities[0] = visible; return visible; } visible = tabs.get(index).isVisible(); visibilities[index] = visible; } return visible; } else { return false; } } } /** The Constant serialVersionUID. */ private static final long serialVersionUID = 1L; /** id used for child panels */ public static final String TAB_PANEL_ID = "panel"; /** The list with the tabs. */ private final List<T> tabs; /** the current tab */ private int currentTab = -1; /** the cache for the visibility. */ private transient VisibilityCache visibilityCache; /** the Container for the unordered list. */ @Getter private WebMarkupContainer tabsUlContainer; /** the Container for the tabs. */ @Getter private WebMarkupContainer tabsContainer; /** the {@link Loop} for the tabs. */ @Getter private Loop tabsLoop; /** * Instantiates a new {@link AjaxCloseableTabbedPanel}. * * @param id * component id * @param tabs * list of ITab objects used to represent tabs */ public AjaxCloseableTabbedPanel(final String id, final List<T> tabs) { this(id, tabs, null); } /** * Instantiates a new {@link AjaxCloseableTabbedPanel}. * * @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 AjaxCloseableTabbedPanel(final String id, final List<T> tabs, final IModel<Integer> model) { super(id, model); setOutputMarkupId(true); setVersioned(false); this.tabs = Args.notNull(tabs, "tabs"); final IModel<Integer> tabCount = new AbstractReadOnlyModel<Integer>() { /** The Constant serialVersionUID. */ private static final long serialVersionUID = 1L; /** * {@inheritDoc} */ @Override public Integer getObject() { return AjaxCloseableTabbedPanel.this.tabs.size(); } }; tabsContainer = newTabsContainer("tabs-container"); add(tabsContainer); tabsUlContainer = newTabsContainer("tabs-ul-container"); tabsContainer.add(tabsUlContainer); // add the loop used to generate tab names tabsUlContainer.add(tabsLoop = newTabsLoop("tabs", tabCount)); add(newPanel()); } /** * Get the css class of the last tab. * * @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 "last"; } /** * Get the selected tab. * * @return index of the selected tab */ public final int getSelectedTab() { return (Integer)getDefaultModelObject(); } /** * Get the css class of the selected tab. * * @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 "selected"; } /** * Get the css class of the tab container. * * @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 "tab-row"; } /** * Gets the list of tabs that can be used by the user to add/remove/reorder tabs in the panel. * * @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; } /** * Gets the visiblity cache. * * @return the visiblity cache */ private VisibilityCache getVisiblityCache() { if (visibilityCache == null) { visibilityCache = new VisibilityCache(); } return visibilityCache; } /** * 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<Integer>(-1); } /** * Factory method for links used to close the selected tab. * * 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 newCloseLink(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 newCloseLink(final String linkId, final int index) { return new AjaxFallbackLink<Void>(linkId) { private static final long serialVersionUID = 1L; @Override public void onClick(final AjaxRequestTarget target) { if (target != null) { onRemoveTab(target, index); } onAjaxUpdate(target); } }; } /** * 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 newCloseTitle(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 AjaxFallbackLink<Void>(linkId) { /** The Constant serialVersionUID. */ private static final long serialVersionUID = 1L; /** * {@inheritDoc} */ @Override public void onClick(final AjaxRequestTarget target) { setSelectedTab(index); if (target != null) { target.add(AjaxCloseableTabbedPanel.this); } onAjaxUpdate(target); } }; } /** * Factory method for the new tab panel. * * @return the new tab panel. */ private WebMarkupContainer newPanel() { return new WebMarkupContainer(TAB_PANEL_ID); } /** * Generates a loop item used to represent a specific tab's <code>li</code> element. * * @param tabIndex * the tab index * @return new loop item */ protected LoopItem newTabContainer(final int tabIndex) { return new LoopItem(tabIndex) { /** The Constant serialVersionUID. */ private static final long serialVersionUID = 1L; /** * {@inheritDoc} */ @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()); } /** * {@inheritDoc} */ @Override protected void onConfigure() { super.onConfigure(); setVisible(getVisiblityCache().isVisible(tabIndex)); } }; } /** * 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) { final WebMarkupContainer wmc = new WebMarkupContainer(id) { /** The Constant serialVersionUID. */ private static final long serialVersionUID = 1L; /** * {@inheritDoc} */ @Override protected void onComponentTag(final ComponentTag tag) { super.onComponentTag(tag); tag.put("class", getTabContainerCssClass()); } }; wmc.setOutputMarkupId(true); return wmc; } /** * Factory method for creating a new {@link Loop} for the tabs. * * @param id * the id * @param model * the model * @return the new {@link Loop} for the tabs. */ protected Loop newTabsLoop(final String id, final IModel<Integer> model) { final Loop localTabsLoop = new Loop(id, model) { /** The Constant serialVersionUID. */ private static final long serialVersionUID = 1L; /** * {@inheritDoc} */ @Override protected LoopItem newItem(final int iteration) { return newTabContainer(iteration); } /** * {@inheritDoc} */ @Override protected void populateItem(final LoopItem item) { final int index = item.getIndex(); final T tab = AjaxCloseableTabbedPanel.this.tabs.get(index); final WebMarkupContainer titleCloseLink = newCloseLink("closeTab", index); titleCloseLink.add(newCloseTitle("closeTitle", tab.getCloseTitle(), index)); item.add(titleCloseLink); final WebMarkupContainer titleLink = newLink("link", index); titleLink.add(newTitle("title", tab.getTitle(), index)); item.add(titleLink); item.add(new AttributeAppender("class", " label")); } }; localTabsLoop.setOutputMarkupId(true); return localTabsLoop; } /** * 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); } /** * A template method that lets users add additional behavior when ajax update occurs. This * method is called after the current tab has been set so access to it can be obtained via * {@link #getSelectedTab()}. * <p> * <strong>Note</strong> Since an {@link AjaxFallbackLink} is used to back the ajax update the * <code>target</code> argument can be null when the client browser does not support ajax and * the fallback mode is used. See {@link AjaxFallbackLink} for details. * * @param target * ajax target used to update this component */ protected void onAjaxUpdate(final AjaxRequestTarget target) { } /** * {@inheritDoc} */ @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(); } /** * {@inheritDoc} */ @Override protected void onDetach() { visibilityCache = null; super.onDetach(); } /** * On new tab. * * @param target * the target * @param tab * the tab */ public void onNewTab(final AjaxRequestTarget target, final T tab) { getTabs().add(tab); setSelectedTab(getTabs().size() - 1); target.add(this); } /** * On new tab. * * @param target * the target * @param tab * the tab * @param index * the index */ public void onNewTab(final AjaxRequestTarget target, final T tab, final int index) { if ((index < 0) || (index >= getTabs().size())) { throw new IndexOutOfBoundsException(); } getTabs().add(index, tab); setSelectedTab(index); target.add(this); } /** * On remove tab removes the tab of the given index. * * @param target * the target * @param index * the index */ public void onRemoveTab(final AjaxRequestTarget target, final int index) { final int tabSize = getTabs().size(); // there have to be at least one tab on the ajaxTabbedPanel... if ((2 <= tabSize) && (index < tabSize)) { setSelectedTab(index); getTabs().remove(index); target.add(this); } } /** * On remove tab removes the given tab if it does exists. * * @param target * the target * @param tab * the tab */ public void onRemoveTab(final AjaxRequestTarget target, final T tab) { final int index = getTabs().indexOf(tab); if (0 <= index) { onRemoveTab(target, index); } } /** * Sets the current tab. * * @param index * the new current tab */ private void setCurrentTab(final int index) { if (this.currentTab == index) { // already current return; } this.currentTab = index; final Component component; if ((currentTab == -1) || tabs.isEmpty() || !getVisiblityCache().isVisible(currentTab)) { // no tabs or the current tab is not visible component = newPanel(); } else { // show panel from selected tab final 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 always have to return a panel with id equal to the provided panelId parameter. TabbedPanel [" + getPath() + "] ITab index [" + currentTab + "]"); } addOrReplace(component); } /** * 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 AjaxCloseableTabbedPanel<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; } }