View Javadoc
1   /*
2   * Copyright 2012-2025 Christophe Friederich
3   *
4   * Licensed under the Apache License, Version 2.0 (the "License");
5   * you may not use this file except in compliance with the License.
6   * You may obtain a copy of the License at
7   *
8   * http://www.apache.org/licenses/LICENSE-2.0
9   *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16  package org.devacfr.maven.skins.reflow.model;
17  
18  import static java.util.Objects.requireNonNull;
19  
20  import com.google.common.base.Strings;
21  import com.google.common.collect.Lists;
22  import java.util.ArrayList;
23  import java.util.Collections;
24  import java.util.List;
25  import javax.annotation.Nonnull;
26  import javax.annotation.Nullable;
27  import org.apache.commons.lang3.builder.ToStringBuilder;
28  import org.codehaus.plexus.util.xml.Xpp3Dom;
29  import org.devacfr.maven.skins.reflow.ISkinConfig;
30  import org.devacfr.maven.skins.reflow.SkinConfigTool;
31  import org.devacfr.maven.skins.reflow.Xpp3Utils;
32  
33  /**
34   * Represents the navside menu component used in document page.
35   * <p>
36   * A sample configuration would be like that:
37   * </p>
38   *
39   * <pre>
40   * {@code
41   * <custom>
42   *   <reflowSkin>
43   *     <pages>
44   *       <document type="doc">
45   *          <menu name="Documentation" selectFirstOnExpand="true">
46   *              <item name="Get Started" href="get-started.html" />
47   *              <item name="Layouts">
48   *                  <item name="Overview" href="layouts.html" />
49   *                  <item name="Body" href="body.html" />
50   *              </item>
51   *              <item name="Migration" href="migration.html" />
52   *          </menu>
53   *       </document>
54   *     </pages>
55   *   </reflowSkin>
56   * </custom>
57   * }∂
58   * </pre>
59   *
60   * @author Christophe Friederich
61   * @since 2.0
62   */
63  public class NavSideMenu extends BsComponent {
64  
65    /** */
66    private static final String COMPONENT = "navside-menu";
67  
68    /** */
69    private static final String MENU_COMPONENT = "menu";
70  
71    /** */
72    private String name;
73  
74    /** */
75    private List<SideNavMenuItem> items;
76  
77    /** */
78    private boolean selectFirstOnExpand = false;
79  
80    /**
81     * Find all {@link SideNavMenuItem sidenav menu items} declared in all document pages.
82     *
83     * @param config
84     *          a config (can <b>not</b> be {@code null}).
85     * @return Returns a list of all all {@link SideNavMenuItem sidenav menu items} declared in all document pages
86     *         (returns list can <b>not</b> be {@code null}).
87     */
88    @Nonnull
89    public static List<SideNavMenuItem> findAllSideNavMenuItems(@Nonnull final ISkinConfig config) {
90      requireNonNull(config);
91      final Xpp3Dom pagesNode = Xpp3Utils.getFirstChild(config.getGlobalProperties(), "pages", config.getNamespace());
92      if (pagesNode == null) {
93        return Collections.emptyList();
94      }
95      final Xpp3Dom[] pages = pagesNode.getChildren();
96      final List<SideNavMenuItem> includePages = new ArrayList<>();
97      for (final Xpp3Dom page : pages) {
98        final String type = page.getAttribute("type");
99        if ("doc".equals(type)) {
100         // This allows preventing accidental reuse of child page in other module of
101         // project
102         final String projectId = page.getAttribute("project");
103         if (!Strings.isNullOrEmpty(projectId) && !projectId.equals(config.getProjectId())) {
104           continue;
105         }
106         final Xpp3Dom menu = page.getChild(MENU_COMPONENT);
107         if (menu == null) {
108           continue;
109         }
110         final String pageName = page.getName();
111         // create a flatten list containing all menuItem.
112         addMenuItemRecursively(includePages, config, menu, pageName, true);
113       }
114     }
115     return includePages;
116   }
117 
118   /**
119    * Default constructor.
120    *
121    * @param config
122    *          a config (can <b>not</b> be {@code null}).
123    */
124   public NavSideMenu(@Nonnull final ISkinConfig config) {
125     super(config, "navside");
126     requireNonNull(config);
127     final Xpp3Dom pageNode = config.getPageProperties();
128     final Xpp3Dom menu = pageNode.getChild(MENU_COMPONENT);
129     final List<SideNavMenuItem> items = Lists.newArrayList();
130     if (menu != null) {
131       final String pageName = pageNode.getName();
132       addMenuItemRecursively(items, config, menu, pageName, false);
133 
134       this.withName(menu.getAttribute("name"))
135           .withItems(items)
136           .withSelectFirstOnSelect(
137             config.getAttributeValue(MENU_COMPONENT, "selectFirstOnExpand", Boolean.class, true));
138       this.setTheme(config.getAttributeValue(COMPONENT, "theme", String.class, "light"));
139       this.setBackground(config.getAttributeValue(COMPONENT, "background", String.class, "light"));
140       this.setCssClass(config.getAttributeValue(COMPONENT, "cssClass", String.class, null));
141     }
142     this.addCssOptions("sidenav-enabled");
143     if (isSelectFirstOnExpand()) {
144       this.addCssOptions("sidenav-select-first-on-select");
145     }
146   }
147 
148   /**
149    * Gets the name of menu displayed on top of navside menu.
150    *
151    * @return Returns a {@link String} representing the name of menu.
152    */
153   @Nullable public String getName() {
154     return name;
155   }
156 
157   /**
158    * Sets the name of menu displayed on top of navside menu.
159    *
160    * @param name
161    *          the name to use.
162    * @return Returns the fluent instance.
163    */
164   protected NavSideMenu withName(final String name) {
165     this.name = name;
166     return this;
167   }
168 
169   /**
170    * Gets the indicating whether menu contains a least one menu item.
171    *
172    * @return Returns {@code true} whether menu contains a least one menu item, otherwise returns {@code false}.
173    */
174   public boolean isHasItems() {
175     return items != null && !items.isEmpty();
176   }
177 
178   /**
179    * @return Returns the {@link List} of {@link SideNavMenuItem} containing in {@code <menu> element}.
180    */
181   @Nonnull
182   public List<SideNavMenuItem> getItems() {
183     return items;
184   }
185 
186   /**
187    * Sets the {@link List} of {@link SideNavMenuItem}.
188    *
189    * @param items
190    *          list of items to use.
191    * @return Returns the fluent instance.
192    */
193   protected NavSideMenu withItems(final List<SideNavMenuItem> items) {
194     this.items = items;
195     return this;
196   }
197 
198   /**
199    * Gets the indicating whether the first sub menu item should be selected when a dropdown menu item is selected and
200    * should expand.
201    *
202    * @return Returns {@code true} whether the first sub menu item should be selected when a dropdown menu item is
203    *         selected and expanded, otherwise returns {@code false}.
204    */
205   public boolean isSelectFirstOnExpand() {
206     return selectFirstOnExpand;
207   }
208 
209   /**
210    * Sets the indicating whether the first sub menu item should be selected when a dropdown menu item is selected and
211    * should expand.
212    *
213    * @param selectFirstOnExpand
214    *          a value to use.
215    * @return Returns the fluent instance.
216    */
217   protected NavSideMenu withSelectFirstOnSelect(final boolean selectFirstOnExpand) {
218     this.selectFirstOnExpand = selectFirstOnExpand;
219     return this;
220   }
221 
222   /**
223    * {@inheritDoc}
224    */
225   @Override
226   public String toString() {
227     return ToStringBuilder.reflectionToString(this);
228   }
229 
230   /**
231    * @param menuItems
232    * @param parentNode
233    * @param pageName
234    * @param flatten
235    */
236   private static void addMenuItemRecursively(@Nonnull final List<SideNavMenuItem> menuItems,
237     @Nonnull final ISkinConfig config,
238     @Nonnull final Xpp3Dom parentNode,
239     @Nonnull final String pageName,
240     final boolean flatten) {
241     for (final Xpp3Dom item : Xpp3Utils.getChildrenNodes(parentNode, "item")) {
242       final String href = item.getAttribute("href");
243       final SideNavMenuItem menuItem = new SideNavMenuItem().withName(item.getAttribute("name"))
244           .withParent(pageName)
245           .withHref(config.relativeLink(href))
246           .withSlugName(SkinConfigTool.slugFilename(href))
247           .withIcon(item.getAttribute("icon"));
248       menuItems.add(menuItem);
249       if (flatten) {
250         addMenuItemRecursively(menuItems, config, item, pageName, true);
251       } else {
252         final List<SideNavMenuItem> list = new ArrayList<>();
253         menuItem.withItems(list);
254         addMenuItemRecursively(list, config, item, pageName, false);
255       }
256     }
257   }
258 }