001/* 002 * Copyright (c) 2003, 2022, Oracle and/or its affiliates. All rights reserved. 003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 004 * 005 * This code is free software; you can redistribute it and/or modify it 006 * under the terms of the GNU General Public License version 2 only, as 007 * published by the Free Software Foundation. Oracle designates this 008 * particular file as subject to the "Classpath" exception as provided 009 * by Oracle in the LICENSE file that accompanied this code. 010 * 011 * This code is distributed in the hope that it will be useful, but WITHOUT 012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 013 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 014 * version 2 for more details (a copy is included in the LICENSE file that 015 * accompanied this code). 016 * 017 * You should have received a copy of the GNU General Public License version 018 * 2 along with this work; if not, write to the Free Software Foundation, 019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 020 * 021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 022 * or visit www.oracle.com if you need additional information or have any 023 * questions. 024 */ 025 026package org.jdrupes.mdoclet.internal.doclets.formats.html; 027 028import java.io.IOException; 029import java.io.Writer; 030import java.util.ArrayList; 031import java.util.Arrays; 032import java.util.HashSet; 033import java.util.List; 034import java.util.Set; 035import java.util.function.Predicate; 036 037import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.ContentBuilder; 038import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.HtmlAttr; 039import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.HtmlId; 040import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.HtmlStyle; 041import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.HtmlTree; 042import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.TagName; 043import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.Text; 044import org.jdrupes.mdoclet.internal.doclets.toolkit.Content; 045 046/** 047 * An HTML container used to display summary tables for various kinds of elements 048 * and other tabular data. 049 * This class historically used to generate an HTML {@code <table>} element but has been 050 * updated to render its content as a stream of {@code <div>} elements that rely on 051 * <a href="https://www.w3.org/TR/css-grid-1/">CSS Grid Layout</a> for styling. 052 * This provides for more flexible layout options, such as splitting up table rows on 053 * small displays. 054 * 055 * <p>The table should be used in three phases: 056 * <ol> 057 * <li>Configuration: the overall characteristics of the table should be specified 058 * <li>Population: the content for the cells in each row should be added 059 * <li>Generation: the HTML content and any associated JavaScript can be accessed 060 * </ol> 061 * 062 * Many methods return the current object, to facilitate fluent builder-style usage. 063 * 064 * A table may support filtered views, which can be selected by clicking on 065 * one of a list of tabs above the table. If the table does not support filtered 066 * views, the caption element is typically displayed as a single (inactive) 067 * tab. The filtered views use a {@link Predicate} to identify the 068 * rows to be shown for each {@link #addTab(Content, Predicate) tab}. The 069 * type parameter for the predicate is the type parameter {@code T} for the table. 070 * The type parameter should be {@link Void} when the table is not configured 071 * to use tabs. 072 * 073 * @param <T> the class or interface used to distinguish the rows to be displayed 074 * for each tab, or {@code Void} when a table does not contain tabs 075 */ 076public class Table<T> extends Content { 077 private final HtmlStyle tableStyle; 078 private Content caption; 079 private List<Tab<T>> tabs; 080 private Set<Tab<T>> occurringTabs; 081 private Content defaultTab; 082 private boolean renderTabs = true; 083 private TableHeader header; 084 private List<HtmlStyle> columnStyles; 085 private HtmlStyle gridStyle; 086 private final List<Content> bodyRows; 087 private HtmlId id; 088 private boolean alwaysShowDefaultTab = false; 089 090 /** 091 * A record containing the data for a table tab. 092 */ 093 record Tab<T>(Content label, Predicate<T> predicate, int index) { 094 } 095 096 /** 097 * Creates a builder for an HTML element representing a table. 098 * 099 * @param tableStyle the style class for the top-level {@code <div>} element 100 */ 101 public Table(HtmlStyle tableStyle) { 102 this.tableStyle = tableStyle; 103 bodyRows = new ArrayList<>(); 104 } 105 106 /** 107 * Sets the caption for the table. 108 * This is ignored if the table is configured to provide tabs to select 109 * different subsets of rows within the table. 110 * 111 * @param captionContent the caption 112 * @return this object 113 */ 114 public Table<T> setCaption(Content captionContent) { 115 caption = getCaption(captionContent); 116 return this; 117 } 118 119 /** 120 * Adds a tab to the table. 121 * Tabs provide a way to display subsets of rows, as determined by a 122 * predicate for the tab, and an item associated with each row. 123 * Tabs will appear left-to-right in the order they are added. 124 * 125 * @param label the tab label 126 * @param predicate the predicate 127 * @return this object 128 */ 129 public Table<T> addTab(Content label, Predicate<T> predicate) { 130 if (tabs == null) { 131 tabs = new ArrayList<>(); // preserves order that tabs are 132 // added 133 occurringTabs = new HashSet<>(); // order not significant 134 } 135 // Use current size of tabs list as id so we have tab ids that are 136 // consistent 137 // across tables with the same tabs but different content. 138 tabs.add(new Tab<>(label, predicate, tabs.size() + 1)); 139 return this; 140 } 141 142 /** 143 * Sets the label for the default tab, which displays all the rows in the table. 144 * This tab will appear first in the left-to-right list of displayed tabs. 145 * 146 * @param label the default tab label 147 * @return this object 148 */ 149 public Table<T> setDefaultTab(Content label) { 150 defaultTab = label; 151 return this; 152 } 153 154 /** 155 * Sets whether to display the default tab even if tabs are empty or only contain a single tab. 156 * @param showDefaultTab true if default tab should always be shown 157 * @return this object 158 */ 159 public Table<T> setAlwaysShowDefaultTab(boolean showDefaultTab) { 160 this.alwaysShowDefaultTab = showDefaultTab; 161 return this; 162 } 163 164 /** 165 * Allows to set whether tabs should be rendered for this table. Some pages use their 166 * own controls to select table categories, in which case the tabs are omitted. 167 * 168 * @param renderTabs true if table tabs should be rendered 169 * @return this object 170 */ 171 public Table<T> setRenderTabs(boolean renderTabs) { 172 this.renderTabs = renderTabs; 173 return this; 174 } 175 176 /** 177 * Sets the header for the table. 178 * 179 * <p>Notes: 180 * <ul> 181 * <li>The column styles are not currently applied to the header, but probably should, eventually 182 * </ul> 183 * 184 * @param header the header 185 * @return this object 186 */ 187 public Table<T> setHeader(TableHeader header) { 188 this.header = header; 189 return this; 190 } 191 192 /** 193 * Sets the styles for be used for the cells in each row. 194 * 195 * <p>Note: 196 * <ul> 197 * <li>The column styles are not currently applied to the header, but probably should, eventually 198 * </ul> 199 * 200 * @param styles the styles 201 * @return this object 202 */ 203 public Table<T> setColumnStyles(HtmlStyle... styles) { 204 return setColumnStyles(Arrays.asList(styles)); 205 } 206 207 /** 208 * Sets the styles for be used for the cells in each row. 209 * 210 * <p>Note: 211 * <ul> 212 * <li>The column styles are not currently applied to the header, but probably should, eventually 213 * </ul> 214 * 215 * @param styles the styles 216 * @return this object 217 */ 218 public Table<T> setColumnStyles(List<HtmlStyle> styles) { 219 columnStyles = styles; 220 return this; 221 } 222 223 /** 224 * Sets the style for the table's grid which controls allocation of space among table columns. 225 * The style should contain a {@code display: grid;} property and its number of columns must 226 * match the number of column styles and content passed to other methods in this class. 227 * 228 * @param gridStyle the grid style 229 * @return this object 230 */ 231 public Table<T> setGridStyle(HtmlStyle gridStyle) { 232 this.gridStyle = gridStyle; 233 return this; 234 } 235 236 /** 237 * Sets the id attribute of the table. 238 * This is required if the table has tabs, in which case a subsidiary id 239 * will be generated for the tabpanel. This subsidiary id is required for 240 * the ARIA support. 241 * 242 * @param id the id 243 * @return this object 244 */ 245 public Table<T> setId(HtmlId id) { 246 this.id = id; 247 return this; 248 } 249 250 /** 251 * Adds a row of data to the table. 252 * Each item of content should be suitable for use as the content of a 253 * {@code <th>} or {@code <td>} cell. 254 * This method should not be used when the table has tabs: use a method 255 * that takes an {@code Element} parameter instead. 256 * 257 * @param contents the contents for the row 258 */ 259 public void addRow(Content... contents) { 260 addRow(null, Arrays.asList(contents)); 261 } 262 263 /** 264 * Adds a row of data to the table. 265 * Each item of content should be suitable for use as the content of a 266 * {@code <th>} or {@code <td> cell}. 267 * This method should not be used when the table has tabs: use a method 268 * that takes an {@code item} parameter instead. 269 * 270 * @param contents the contents for the row 271 */ 272 public void addRow(List<Content> contents) { 273 addRow(null, contents); 274 } 275 276 /** 277 * Adds a row of data to the table. 278 * Each item of content should be suitable for use as the content of a 279 * {@code <th>} or {@code <td>} cell. 280 * 281 * If tabs have been added to the table, the specified item will be used 282 * to determine whether the row should be displayed when any particular tab 283 * is selected, using the predicate specified when the tab was 284 * {@link #addTab(Content, Predicate) added}. 285 * 286 * @param item the item 287 * @param contents the contents for the row 288 * @throws NullPointerException if tabs have previously been added to the table 289 * and {@code item} is null 290 */ 291 public void addRow(T item, Content... contents) { 292 addRow(item, Arrays.asList(contents)); 293 } 294 295 /** 296 * Adds a row of data to the table. 297 * Each item of content should be suitable for use as the content of a 298 * {@code <div>} cell. 299 * 300 * If tabs have been added to the table, the specified item will be used 301 * to determine whether the row should be displayed when any particular tab 302 * is selected, using the predicate specified when the tab was 303 * {@link #addTab(Content, Predicate) added}. 304 * 305 * @param item the item 306 * @param contents the contents for the row 307 * @throws NullPointerException if tabs have previously been added to the table 308 * and {@code item} is null 309 */ 310 public void addRow(T item, List<Content> contents) { 311 if (tabs != null && item == null) { 312 throw new NullPointerException(); 313 } 314 if (contents.size() != columnStyles.size()) { 315 throw new IllegalArgumentException( 316 "row content size does not match number of columns"); 317 } 318 319 Content row = new ContentBuilder(); 320 321 int rowIndex = bodyRows.size(); 322 HtmlStyle rowStyle = rowIndex % 2 == 0 ? HtmlStyle.evenRowColor 323 : HtmlStyle.oddRowColor; 324 325 List<String> tabClasses = new ArrayList<>(); 326 if (tabs != null) { 327 // Construct a series of values to add to the HTML 'class' attribute 328 // for the cells of 329 // this row, such that there is a default value and a value 330 // corresponding to each tab 331 // whose predicate matches the item. The values correspond to the 332 // equivalent ids. 333 // The values are used to determine the cells to make visible when a 334 // tab is selected. 335 tabClasses.add(id.name()); 336 for (var tab : tabs) { 337 if (tab.predicate().test(item)) { 338 occurringTabs.add(tab); 339 tabClasses.add(HtmlIds.forTab(id, tab.index()).name()); 340 } 341 } 342 } 343 int colIndex = 0; 344 for (Content c : contents) { 345 HtmlStyle cellStyle = columnStyles.get(colIndex); 346 // Always add content to make sure the cell isn't dropped 347 var cell = HtmlTree.DIV(cellStyle) 348 .addUnchecked(c.isEmpty() ? Text.EMPTY : c); 349 cell.addStyle(rowStyle); 350 351 for (String tabClass : tabClasses) { 352 cell.addStyle(tabClass); 353 } 354 row.add(cell); 355 colIndex++; 356 } 357 bodyRows.add(row); 358 } 359 360 /** 361 * Returns whether the table is empty. 362 * The table is empty if it has no (body) rows. 363 * 364 * @return true if the table has no rows 365 */ 366 public boolean isEmpty() { 367 return bodyRows.isEmpty(); 368 } 369 370 @Override 371 public boolean write(Writer out, String newline, boolean atNewline) 372 throws IOException { 373 return toContent().write(out, newline, atNewline); 374 } 375 376 /** 377 * Returns the HTML for the table. 378 * 379 * @return the HTML 380 */ 381 private Content toContent() { 382 Content main; 383 if (id != null) { 384 main = new HtmlTree(TagName.DIV).setId(id); 385 } else { 386 main = new ContentBuilder(); 387 } 388 // If no grid style is set use on of the default styles 389 if (gridStyle == null) { 390 gridStyle = switch (columnStyles.size()) { 391 case 2 -> HtmlStyle.twoColumnSummary; 392 case 3 -> HtmlStyle.threeColumnSummary; 393 case 4 -> HtmlStyle.fourColumnSummary; 394 default -> throw new IllegalStateException(); 395 }; 396 } 397 398 var table = HtmlTree.DIV(tableStyle).addStyle(gridStyle); 399 if ((tabs == null || occurringTabs.size() == 1) 400 && !alwaysShowDefaultTab) { 401 if (tabs == null) { 402 main.add(caption); 403 } else { 404 main.add(getCaption(occurringTabs.iterator().next().label())); 405 } 406 table.add(getTableBody()); 407 main.add(table); 408 } else { 409 var tablist = HtmlTree.DIV(HtmlStyle.tableTabs) 410 .put(HtmlAttr.ROLE, "tablist") 411 .put(HtmlAttr.ARIA_ORIENTATION, "horizontal"); 412 413 HtmlId defaultTabId = HtmlIds.forTab(id, 0); 414 if (renderTabs) { 415 tablist.add(createTab(defaultTabId, HtmlStyle.activeTableTab, 416 true, defaultTab)); 417 } else { 418 tablist.add(getCaption(defaultTab)); 419 } 420 table.put(HtmlAttr.ARIA_LABELLEDBY, defaultTabId.name()); 421 if (renderTabs) { 422 for (var tab : tabs) { 423 if (occurringTabs.contains(tab)) { 424 tablist.add(createTab(HtmlIds.forTab(id, tab.index()), 425 HtmlStyle.tableTab, false, tab.label())); 426 } 427 } 428 } 429 if (id == null) { 430 throw new IllegalStateException("no id set for table"); 431 } 432 var tabpanel = new HtmlTree(TagName.DIV) 433 .setId(HtmlIds.forTabPanel(id)) 434 .put(HtmlAttr.ROLE, "tabpanel"); 435 table.add(getTableBody()); 436 tabpanel.add(table); 437 main.add(tablist); 438 main.add(tabpanel); 439 } 440 return main; 441 } 442 443 private HtmlTree createTab(HtmlId tabId, HtmlStyle style, 444 boolean defaultTab, Content tabLabel) { 445 var tab = new HtmlTree(TagName.BUTTON) 446 .setId(tabId) 447 .put(HtmlAttr.ROLE, "tab") 448 .put(HtmlAttr.ARIA_SELECTED, defaultTab ? "true" : "false") 449 .put(HtmlAttr.ARIA_CONTROLS, HtmlIds.forTabPanel(id).name()) 450 .put(HtmlAttr.TABINDEX, defaultTab ? "0" : "-1") 451 .put(HtmlAttr.ONKEYDOWN, "switchTab(event)") 452 .put(HtmlAttr.ONCLICK, 453 "show('" + id.name() + "', '" + (defaultTab ? id : tabId).name() 454 + "', " + columnStyles.size() + ")") 455 .setStyle(style); 456 tab.add(tabLabel); 457 return tab; 458 } 459 460 private Content getTableBody() { 461 ContentBuilder tableContent = new ContentBuilder(); 462 tableContent.add(header); 463 bodyRows.forEach(tableContent::add); 464 return tableContent; 465 } 466 467 private HtmlTree getCaption(Content title) { 468 return HtmlTree.DIV(HtmlStyle.caption, HtmlTree.SPAN(title)); 469 } 470}