/* * Copyright (c) 2003, 2019, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package jdk.javadoc.internal.doclets.formats.html.markup; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.function.IntFunction; import java.util.function.Predicate; import javax.lang.model.element.Element; import jdk.javadoc.internal.doclets.formats.html.Contents; import jdk.javadoc.internal.doclets.toolkit.Content; /** * A builder for HTML tables, such as the summary tables for various * types of element. * *

The table should be used in three phases: *

    *
  1. Configuration: the overall characteristics of the table should be specified *
  2. Population: the content for the cells in each row should be added *
  3. Generation: the HTML content and any associated JavaScript can be accessed *
* * Many methods return the current object, to facilitate fluent builder-style usage. * *

This is NOT part of any supported API. * If you write code that depends on this, you do so at your own risk. * This code and its internal interfaces are subject to change or * deletion without notice. */ public class Table { private final HtmlStyle tableStyle; private Content caption; private Map> tabMap; private String defaultTab; private Set tabs; private HtmlStyle activeTabStyle = HtmlStyle.activeTableTab; private HtmlStyle tabStyle = HtmlStyle.tableTab; private HtmlStyle tabEnd = HtmlStyle.tabEnd; private IntFunction tabScript; private Function tabId = (i -> "t" + i); private TableHeader header; private List columnStyles; private int rowScopeColumnIndex; private List stripedStyles = Arrays.asList(HtmlStyle.altColor, HtmlStyle.rowColor); private final List bodyRows; private final List bodyRowMasks; private String rowIdPrefix = "i"; private String id; /** * Creates a builder for an HTML table. * * @param style the style class for the {@code } tag */ public Table(HtmlStyle style) { this.tableStyle = style; bodyRows = new ArrayList<>(); bodyRowMasks = new ArrayList<>(); } /** * Sets the caption for the table. * This is ignored if the table is configured to provide tabs to select * different subsets of rows within the table. * The caption should be suitable for use as the content of a {@code } tags, to give a "striped" appearance. * The defaults are currently {@code rowColor} and {@code altColor}. * * @param evenRowStyle the style to use for even-numbered rows * @param oddRowStyle the style to use for odd-numbered rows * @return */ public Table setStripedStyles(HtmlStyle evenRowStyle, HtmlStyle oddRowStyle) { stripedStyles = Arrays.asList(evenRowStyle, oddRowStyle); return this; } /** * Sets the column used to indicate which cell in a row should be declared * as a header cell with the {@code scope} attribute set to {@code row}. * * @param columnIndex the column index * @return this object */ public Table setRowScopeColumn(int columnIndex) { rowScopeColumnIndex = columnIndex; return this; } /** * Sets the styles for be used for the cells in each row. * *

Note: *

    *
  • The column styles are not currently applied to the header, but probably should, eventually *
* * @param styles the styles * @return this object */ public Table setColumnStyles(HtmlStyle... styles) { return setColumnStyles(Arrays.asList(styles)); } /** * Sets the styles for be used for the cells in each row. * *

Note: *

    *
  • The column styles are not currently applied to the header, but probably should, eventually *
* * @param styles the styles * @return this object */ public Table setColumnStyles(List styles) { columnStyles = styles; return this; } /** * Sets the prefix used for the {@code id} attribute for each row in the table. * The default is "i". * *

Note: *

    *
  • The prefix should probably be a value such that the generated ids cannot * clash with any other id, such as those that might be created for fields within * a class. *
* * @param prefix the prefix * @return this object */ public Table setRowIdPrefix(String prefix) { rowIdPrefix = prefix; return this; } /** * Sets the id attribute of the table. * * @param id the id * @return this object */ public Table setId(String id) { this.id = id; return this; } /** * Add a row of data to the table. * Each item of content should be suitable for use as the content of a * {@code
} * element. * * For compatibility, the code currently accepts a {@code } element * as well. This should be removed when all clients rely on using the {@code } * element being generated by this class. * * @param captionContent the caption * @return this object */ public Table setCaption(Content captionContent) { if (captionContent instanceof HtmlTree && ((HtmlTree) captionContent).htmlTag == HtmlTag.CAPTION) { caption = captionContent; } else { caption = getCaption(captionContent); } return this; } /** * Adds a tab to the table. * Tabs provide a way to display subsets of rows, as determined by a * predicate for the tab, and an element associated with each row. * Tabs will appear left-to-right in the order they are added. * * @param name the name of the tab * @param predicate the predicate * @return this object */ public Table addTab(String name, Predicate predicate) { if (tabMap == null) { tabMap = new LinkedHashMap<>(); // preserves order that tabs are added tabs = new HashSet<>(); // order not significant } tabMap.put(name, predicate); return this; } /** * Sets the name for the default tab, which displays all the rows in the table. * This tab will appear first in the left-to-right list of displayed tabs. * * @param name the name * @return this object */ public Table setDefaultTab(String name) { defaultTab = name; return this; } /** * Sets the function used to generate the JavaScript to be used when a tab is selected. * When the function is invoked, the argument will be an integer value containing * the bit mask identifying the rows to be selected. * * @param f the function * @return this object */ public Table setTabScript(IntFunction f) { tabScript = f; return this; } /** * Sets the name of the styles used to display the tabs. * * @param activeTabStyle the style for the active tab * @param tabStyle the style for other tabs * @param tabEnd the style for the padding that appears within each tab * @return this object */ public Table setTabStyles(HtmlStyle activeTabStyle, HtmlStyle tabStyle, HtmlStyle tabEnd) { this.activeTabStyle = activeTabStyle; this.tabStyle = tabStyle; this.tabEnd = tabEnd; return this; } /** * Sets the JavaScript function used to generate the {@code id} attribute for each tag. * The default is to use tN where N is the index of the tab, * counting from 0 (for the default tab), and then from 1 upwards for additional tabs. * * @param f the function * @return this object */ public Table setTabId(Function f) { tabId = f; return this; } /** * Sets the header for the table. * *

Notes: *

    *
  • The column styles are not currently applied to the header, but probably should, eventually *
* * @param header the header * @return this object */ public Table setHeader(TableHeader header) { this.header = header; return this; } /** * Sets the styles used for {@code
} or {@code } cell. * This method should not be used when the table has tabs: use a method * that takes an {@code Element} parameter instead. * * @param contents the contents for the row */ public void addRow(Content... contents) { addRow(null, Arrays.asList(contents)); } /** * Add a row of data to the table. * Each item of content should be suitable for use as the content of a * {@code } or {@code cell}. * This method should not be used when the table has tabs: use a method * that takes an {@code element} parameter instead. * * @param contents the contents for the row */ public void addRow(List contents) { addRow(null, contents); } /** * Add a row of data to the table. * Each item of content should be suitable for use as the content of a * {@code } or {@code } cell. * * If tabs have been added to the table, the specified element will be used * to determine whether the row should be displayed when any particular tab * is selected, using the predicate specified when the tab was * {@link #add(String,Predicate) added}. * * @param element the element * @param contents the contents for the row * @throws NullPointerException if tabs have previously been added to the table * and {@code element} is null */ public void addRow(Element element, Content... contents) { addRow(element, Arrays.asList(contents)); } /** * Add a row of data to the table. * Each item of content should be suitable for use as the content of a * {@code } or {@code } cell. * * If tabs have been added to the table, the specified element will be used * to determine whether the row should be displayed when any particular tab * is selected, using the predicate specified when the tab was * {@link #add(String,Predicate) added}. * * @param element the element * @param contents the contents for the row * @throws NullPointerException if tabs have previously been added to the table * and {@code element} is null */ public void addRow(Element element, List contents) { if (tabMap != null && element == null) { throw new NullPointerException(); } HtmlTree row = new HtmlTree(HtmlTag.TR); if (stripedStyles != null) { int rowIndex = bodyRows.size(); row.put(HtmlAttr.CLASS, stripedStyles.get(rowIndex % 2).name()); } int colIndex = 0; for (Content c : contents) { HtmlStyle cellStyle = (columnStyles == null || colIndex > columnStyles.size()) ? null : columnStyles.get(colIndex); HtmlTree cell = (colIndex == rowScopeColumnIndex) ? HtmlTree.TH(cellStyle, "row", c) : HtmlTree.TD(cellStyle, c); row.add(cell); colIndex++; } bodyRows.add(row); if (tabMap != null) { int index = bodyRows.size() - 1; row.put(HtmlAttr.ID, (rowIdPrefix + index)); int mask = 0; int maskBit = 1; for (Map.Entry> e : tabMap.entrySet()) { String name = e.getKey(); Predicate predicate = e.getValue(); if (predicate.test(element)) { tabs.add(name); mask |= maskBit; } maskBit = (maskBit << 1); } bodyRowMasks.add(mask); } } /** * Returns whether or not the table is empty. * The table is empty if it has no (body) rows. * * @return true if the table has no rows */ public boolean isEmpty() { return bodyRows.isEmpty(); } /** * Returns the HTML for the table. * * @return the HTML */ public Content toContent() { HtmlTree mainDiv = new HtmlTree(HtmlTag.DIV); mainDiv.setStyle(tableStyle); if (id != null) { mainDiv.setId(id); } HtmlTree table = new HtmlTree(HtmlTag.TABLE); if (tabMap == null || tabs.size() == 1) { if (tabMap == null) { table.add(caption); } else if (tabs.size() == 1) { String tabName = tabs.iterator().next(); table.add(getCaption(new StringContent(tabName))); } table.add(getTableBody()); mainDiv.add(table); } else { HtmlTree tablist = new HtmlTree(HtmlTag.DIV) .put(HtmlAttr.ROLE, "tablist") .put(HtmlAttr.ARIA_ORIENTATION, "horizontal"); int tabIndex = 0; tablist.add(createTab(tabId.apply(tabIndex), activeTabStyle, true, defaultTab)); table.put(HtmlAttr.ARIA_LABELLEDBY, tabId.apply(tabIndex)); for (String tabName : tabMap.keySet()) { tabIndex++; if (tabs.contains(tabName)) { String script = tabScript.apply(1 << (tabIndex - 1)); HtmlTree tab = createTab(tabId.apply(tabIndex), tabStyle, false, tabName); tab.put(HtmlAttr.ONCLICK, script); tablist.add(tab); } } HtmlTree tabpanel = new HtmlTree(HtmlTag.DIV) .put(HtmlAttr.ID, tableStyle + "_tabpanel") .put(HtmlAttr.ROLE, "tabpanel"); table.add(getTableBody()); tabpanel.add(table); mainDiv.add(tablist); mainDiv.add(tabpanel); } return mainDiv; } private HtmlTree createTab(String tabId, HtmlStyle style, boolean defaultTab, String tabName) { HtmlTree tab = new HtmlTree(HtmlTag.BUTTON) .put(HtmlAttr.ROLE, "tab") .put(HtmlAttr.ARIA_SELECTED, defaultTab ? "true" : "false") .put(HtmlAttr.ARIA_CONTROLS, tableStyle + "_tabpanel") .put(HtmlAttr.TABINDEX, defaultTab ? "0" : "-1") .put(HtmlAttr.ONKEYDOWN, "switchTab(event)") .put(HtmlAttr.ID, tabId) .setStyle(style); tab.add(tabName); return tab; } private Content getTableBody() { ContentBuilder tableContent = new ContentBuilder(); Content thead = new HtmlTree(HtmlTag.THEAD); thead.add(header.toContent()); tableContent.add(thead); Content tbody = new HtmlTree(HtmlTag.TBODY); bodyRows.forEach(row -> tbody.add(row)); tableContent.add(tbody); return tableContent; } /** * Returns whether or not the table needs JavaScript support. * It requires such support if tabs have been added. * * @return true if JavaScript is required */ public boolean needsScript() { return (tabs != null) && (tabs.size() > 1); } /** * Returns the script to be used in conjunction with the table. * * @return the script */ public String getScript() { if (tabMap == null) throw new IllegalStateException(); StringBuilder sb = new StringBuilder(); // Add the variable defining the bitmask for each row sb.append("var data").append(" = {"); int rowIndex = 0; for (int mask : bodyRowMasks) { if (rowIndex > 0) { sb.append(","); } sb.append("\"").append(rowIdPrefix).append(rowIndex).append("\":").append(mask); rowIndex++; } sb.append("};\n"); // Add the variable defining the tabs sb.append("var tabs = {"); appendTabInfo(sb, 65535, tabId.apply(0), defaultTab); int tabIndex = 1; int maskBit = 1; for (String tabName: tabMap.keySet()) { if (tabs.contains(tabName)) { sb.append(","); appendTabInfo(sb, maskBit, tabId.apply(tabIndex), tabName); } tabIndex++; maskBit = (maskBit << 1); } sb.append("};\n"); // Add the variables defining the stylenames appendStyleInfo(sb, stripedStyles.get(0), stripedStyles.get(1), tabStyle, activeTabStyle); return sb.toString(); } private void appendTabInfo(StringBuilder sb, int value, String id, String name) { sb.append(value) .append(":[") .append(Script.stringLiteral(id)) .append(",") .append(Script.stringLiteral(name)) .append("]"); } private void appendStyleInfo(StringBuilder sb, HtmlStyle... styles) { for (HtmlStyle style : styles) { sb.append("var ").append(style).append(" = \"").append(style).append("\";\n"); } } private HtmlTree getCaption(Content title) { return new HtmlTree(HtmlTag.CAPTION, HtmlTree.SPAN(title), HtmlTree.SPAN(tabEnd, Entity.NO_BREAK_SPACE)); } }