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>&gt;&gt;</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 &lt;ul&gt; &lt;li&gt; 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}