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&lt;String&gt;(&quot;first tab&quot;)) {
048 *   public Panel getPanel(String panelId)
049 *   {
050 *     return new TabPanel1(panelId);
051 *   }
052 * });
053 * 
054 * tabs.add(new AbstractTab(new Model&lt;String&gt;(&quot;second tab&quot;)) {
055 *   public Panel getPanel(String panelId)
056 *   {
057 *     return new TabPanel2(panelId);
058 *   }
059 * });
060 * 
061 * add(new TabbedPanel(&quot;tabs&quot;, tabs));
062 * 
063 * &lt;span wicket:id=&quot;tabs&quot; class=&quot;tabpanel&quot;&gt;[tabbed panel will be here]&lt;/span&gt;
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         * &lt;a href=&quot;#&quot; wicket:id=&quot;link&quot;&gt;&lt;span wicket:id=&quot;title&quot;&gt;[[tab title]]&lt;/span&gt;&lt;/a&gt;
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}