/**
* OLAT - Online Learning and Training<br>
* http://www.olat.org
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br>
* University of Zurich, Switzerland.
* <hr>
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* This file has been modified by the OpenOLAT community. Changes are licensed
* under the Apache 2.0 license as the original file.
* <p>
*/
package org.olat.core.gui.components.table;
import java.util.List;
import org.apache.commons.lang.StringEscapeUtils;
import org.olat.core.gui.components.Component;
import org.olat.core.gui.components.DefaultComponentRenderer;
import org.olat.core.gui.components.form.flexible.impl.NameValuePair;
import org.olat.core.gui.control.winmgr.AJAXFlags;
import org.olat.core.gui.render.RenderResult;
import org.olat.core.gui.render.Renderer;
import org.olat.core.gui.render.StringOutput;
import org.olat.core.gui.render.URLBuilder;
import org.olat.core.gui.translator.Translator;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
/**
* enclosing_type Description: <br>
*
* @author Felix Jost
*/
public class TableRenderer extends DefaultComponentRenderer {
protected static final String TABLE_MULTISELECT_GROUP = "tb_ms";
private static final OLog log = Tracing.createLoggerFor(TableRenderer.class);
/**
* @see org.olat.core.gui.render.ui.ComponentRenderer#render(org.olat.core.gui.render.Renderer, org.olat.core.gui.render.StringOutput, org.olat.core.gui.components.Component,
* org.olat.core.gui.render.URLBuilder, org.olat.core.gui.translator.Translator, org.olat.core.gui.render.RenderResult, java.lang.String[])
*/
@Override
public void render(final Renderer renderer, final StringOutput target, final Component source, final URLBuilder ubu, final Translator translator, final RenderResult renderResult,
final String[] args) {
long start = 0;
if (log.isDebug()) {
start = System.currentTimeMillis();
}
assert (source instanceof Table);
Table table = (Table) source;
boolean iframePostEnabled = renderer.getGlobalSettings().getAjaxFlags().isIframePostEnabled();
int rows = table.getRowCount();
int cols = table.getColumnCount();
boolean selRowUnSelectable = table.isSelectedRowUnselectable();
// the really selected rowid (from the tabledatamodel)
int selRowId = table.getSelectedRowId();
int resultsPerPage = table.getResultsPerPage();
Integer currentPageId = table.getCurrentPageId();
boolean usePageing;
int startRowId = 0;
int endRowId = rows;
// initalize pageing
if (table.isPageingEnabled() && currentPageId != null && !table.isShowAllSelected()) {
startRowId = ((currentPageId.intValue() - 1) * resultsPerPage);
endRowId = startRowId + resultsPerPage;
if (endRowId > rows) {
endRowId = rows;
}
usePageing = true;
} else {
startRowId = 0;
endRowId = rows;
usePageing = false;
}
// Render table wrapper and table
String formName = renderMultiselectForm(target, source, ubu, iframePostEnabled);
target.append("<div class=\"o_table_wrapper\" id=\"o_table_wrapper_").append(table.hashCode()).append("\">")
.append("<table id=\"o_table").append(table.hashCode()).append("\" class=\"o_table table table-striped table-condensed table-hover")
.append(" table-bordered", table.isDisplayTableGrid())
.append("\">");
appendHeaderLinks(target, translator, table, cols, iframePostEnabled, ubu);
appendDataRows(renderer, target, ubu, table, iframePostEnabled, cols, selRowUnSelectable, selRowId, startRowId, endRowId);
target.append("</table><div class='o_table_footer'>");
appendSelectDeselectAllButtons(target, translator, table, formName, rows, resultsPerPage, iframePostEnabled, ubu);
appendTablePageing(target, translator, table, rows, resultsPerPage, currentPageId, usePageing, iframePostEnabled, ubu);
appendMultiselectFormActions(target, formName, translator, table);
target.append("</div></div>")
// lastly close multiselect
.append("</form>");
appendViewportResizeJsFix(target, source, rows, usePageing);
if (log.isDebug()) {
long duration = System.currentTimeMillis() - start;
log.debug("Perf-Test: render takes " + duration);
}
}
private void appendViewportResizeJsFix(final StringOutput target, final Component source, int rows, boolean usePageing) {
// JS code to resize table to browser view port and display scrollbars in the table
// when not in pageing mode and more than 1000 results are shown.
// This prevents a very strange overflow problem in FF that makes all
// entries after the 1023 entry or even the entire table unreadable.
// Comment CDATA section to make it work with prototype's stripScripts method !
if (!usePageing && rows > 1000) {
target.append("<script type=\"text/javascript\">/* <![CDATA[ */\n ")
.append("jQuery(function() { jQuery('#o_table_wrapper").append(source.hashCode()).append("').height(o_viewportHeight()/3*2);});")
.append("/* ]]> */\n</script>");
}
}
private void appendMultiselectFormActions(StringOutput target, String formName, Translator translator, Table table) {
// add multiselect form actions
List<TableMultiSelect> multiSelectActions = table.getMultiSelectActions();
if (multiSelectActions.size() > 0) {
target.append("<div class=\"o_table_buttons\">");
for (TableMultiSelect action: multiSelectActions) {
String multiSelectActionIdentifer = action.getAction();
String value;
if(action.getI18nKey() != null) {
value = StringEscapeUtils.escapeHtml(translator.translate(action.getI18nKey()));
} else {
value = action.getLabel();
}
target.append("<button type=\"button\" name=\"").append(multiSelectActionIdentifer)
.append("\" class=\"btn btn-default\" onclick=\"o_TableMultiActionEvent('").append(formName).append("','").append(multiSelectActionIdentifer).append("');\"><span>").append(value).append("</span></button> ");
}
target.append("</div>");
}
}
private void appendTablePageing(StringOutput target, Translator translator, Table table, int rows,
int resultsPerPage, Integer currentPageId, boolean usePageing, boolean ajaxEnabled, URLBuilder ubu) {
if (usePageing && (rows > resultsPerPage)) {
int pageid = currentPageId.intValue();
// paging bug OLAT-935 part missing second page, or missing last page due rounding issues.
int maxpageid = (int) Math.ceil(((double) rows / (double) resultsPerPage));
target.append("<div class='o_table_pagination'><ul class='pagination'>");
String formName = "tb_ms_" + table.hashCode();
appendTablePageingBackLink(target, formName, pageid);
appendPageNumberLinks(target, formName, pageid, maxpageid, ubu);
appendTablePageingNextLink(target, formName, rows, resultsPerPage, pageid);
appendTablePageingShowallLink(target, translator, table, ajaxEnabled, ubu);
target.append("</ul></div>");
}
}
private void appendTablePageingShowallLink(StringOutput sb, Translator translator, Table table, boolean ajaxEnabled, URLBuilder ubu) {
if (table.isShowAllLinkEnabled()) {
sb.append("<li><a ");
ubu.buildHrefAndOnclick(sb, ajaxEnabled,
new NameValuePair(Table.FORM_CMD,Table.COMMAND_PAGEACTION),
new NameValuePair(Table.FORM_PARAM, Table.COMMAND_PAGEACTION_SHOWALL))
.append(">").append(translator.translate("table.showall")).append("</a></li>");
}
}
private void appendTablePageingNextLink(StringOutput target, String formName, int rows, int resultsPerPage, int pageid) {
boolean enabled = ((pageid * resultsPerPage) < rows);
target.append("<li").append(" class='disabled'", !enabled).append("><a ");
if(enabled) {
target.append(" href=\"javascript:;\" onclick=\"o_XHRSubmit('")
.append(formName).append("','").append(Table.FORM_CMD).append("','").append(Table.COMMAND_PAGEACTION)
.append("','").append(Table.FORM_PARAM).append("','").append(Table.COMMAND_PAGEACTION_FORWARD).append("'); return false;\"");
} else {
target.append("href=\"javascript:;\"");
}
target.append(">»").append("</a></li>");
}
private void appendTablePageingBackLink(StringOutput target, String formName, int pageid) {
boolean enabled = pageid > 1;
target.append("<li").append(" class='disabled'", !enabled).append("><a ");
if(enabled) {
target.append(" href=\"javascript:;\" onclick=\"o_XHRSubmit('")
.append(formName).append("','").append(Table.FORM_CMD).append("','").append(Table.COMMAND_PAGEACTION)
.append("','").append(Table.FORM_PARAM).append("','").append(Table.COMMAND_PAGEACTION_BACKWARD).append("'); return false;\"");
} else {
target.append("href=\"javascript:;\"");
}
target.append(">«").append("</a>");
}
private void appendSelectDeselectAllButtons(StringOutput target, Translator translator, Table table, String formName, int rows, int resultsPerPage,
boolean ajaxEnabled, URLBuilder ubu) {
if (table.isMultiSelect() && !table.isMultiSelectAsDisabled()) {
target.append("<div class='o_table_checkall input-sm'>")
.append("<label class='checkbox-inline'>")
.append("<a href='#' onclick=\"javascript:o_table_toggleCheck('").append(formName).append("', true)\">")
.append("<i class='o_icon o_icon-lg o_icon_check_on'> </i> ")
.append(translator.translate("checkall"))
.append("</a></label>");
target.append("<label class='checkbox-inline'><a href=\"#\" onclick=\"javascript:o_table_toggleCheck('").append(formName).append("', false)\">")
.append("<i class='o_icon o_icon-lg o_icon_check_off'> </i> ")
.append(translator.translate("uncheckall"))
.append("</a></label>")
.append("</div>");
}
if (table.isShowAllSelected() && (rows > resultsPerPage)) {
target.append("<div class='o_table_pagination'><ul class='pagination'><li>")
.append("<a class=\"").append("btn btn-sm btn-default").append("\" ");
ubu.buildHrefAndOnclick(target, ajaxEnabled,
new NameValuePair(Table.FORM_CMD, Table.COMMAND_PAGEACTION),
new NameValuePair(Table.FORM_PARAM, Table.COMMAND_SHOW_PAGES))
.append(">")
.append(translator.translate("table.showpages")).append("</a>")
.append("</li><ul></div>");
}
}
private void appendDataRows(final Renderer renderer, final StringOutput target, final URLBuilder ubu, Table table, boolean iframePostEnabled, int cols, boolean selRowUnSelectable, int selRowId,
int startRowId, int endRowId) {
target.append("<tbody>");
long startRowLoop = 0;
if (log.isDebug()) {
startRowLoop = System.currentTimeMillis();
}
for (int i = startRowId; i < endRowId; i++) {
String cssClass = "";
// the position of the selected row in the tabledatamodel
int currentPosInModel = table.getSortedRow(i);
boolean isMark = selRowUnSelectable && (selRowId == currentPosInModel);
TableDataModel<?> model = table.getTableDataModel();
if (model instanceof TableDataModelWithMarkableRows) {
TableDataModelWithMarkableRows<?> markableModel = (TableDataModelWithMarkableRows<?>) model;
String rowCss = markableModel.getRowCssClass(currentPosInModel);
if (rowCss != null) {
cssClass += " " + rowCss;
}
}
target.append("<tr class=\"").append(cssClass).append("\">");
appendSingleDataRow(renderer, target, ubu, table, iframePostEnabled, cols, i, currentPosInModel, isMark);
target.append("</tr>");
}
if (log.isDebug()) {
long durationRowLoop = System.currentTimeMillis() - startRowLoop;
log.debug("Perf-Test: render.durationRowLoop takes " + durationRowLoop);
}
// end of table table
target.append("</tbody>");
}
private void appendSingleDataRow(final Renderer renderer, final StringOutput target, final URLBuilder ubu, Table table, final boolean iframePostEnabled, final int cols, final int i,
final int currentPosInModel, final boolean isMark) {
String cssClass;
for (int j = 0; j < cols; j++) {
ColumnDescriptor cd = table.getColumnDescriptor(j);
int alignment = cd.getAlignment();
cssClass = (alignment == ColumnDescriptor.ALIGNMENT_LEFT ? "text-left" : (alignment == ColumnDescriptor.ALIGNMENT_RIGHT ? "text-right" : "text-center"));
target.append("<td class=\"").append(cssClass);
if (isMark) {
target.append(" o_table_marked");
}
target.append("\">");
String action = cd.getAction(i);
if (action != null) {
StringOutput so = new StringOutput(100);
cd.renderValue(so, i, renderer);
appendSingleDataRowActionColumn(target, ubu, table, iframePostEnabled, i, currentPosInModel, j, cd, action, so.toString());
} else {
cd.renderValue(target, i, renderer);
}
target.append("</td>");
}
}
private void appendSingleDataRowActionColumn(StringOutput target, URLBuilder ubu, Table table, boolean ajaxEnabled, int i, int currentPosInModel, int j,
ColumnDescriptor cd, String action, String renderval) {
// If we have actions on the table rows, we just submit traditional style (not via form.submit())
// Note that changes in the state of multiselects will not be reflected in the model.
target.append("<a ");
if (cd.isPopUpWindowAction()) {
// render as popup window
target.append("href=\"javascript:{var win=window.open('");
ubu.buildURI(target, new String[] { Table.COMMANDLINK_ROWACTION_CLICKED, Table.COMMANDLINK_ROWACTION_ID }, new String[] { String.valueOf(currentPosInModel), action }); // url
target.append("','tw_").append(i + "_" + j); // javascript window name
target.append("','");
String popUpAttributes = cd.getPopUpWindowAttributes();
if (popUpAttributes != null) {
target.append(popUpAttributes);
}
target.append("');win.focus();}\">");
} else {
// render in same window
ubu.buildHrefAndOnclick(target, null, ajaxEnabled, !table.isSuppressDirtyFormWarning(), true,
new NameValuePair(Table.COMMANDLINK_ROWACTION_CLICKED, currentPosInModel),
new NameValuePair(Table.COMMANDLINK_ROWACTION_ID, action)).append(">");
}
target.append(renderval).append("</a>");
}
private void appendHeaderLinks(final StringOutput target, final Translator translator, Table table, int cols, boolean ajaxEnabled, URLBuilder ubu) {
if (!table.isDisplayTableHeader()) return;
target.append("<thead><tr>");
for (int i = 0; i < cols; i++) {
ColumnDescriptor cd = table.getColumnDescriptor(i);
String header;
if (cd.translateHeaderKey()) {
header = translator.translate(cd.getHeaderKey());
} else {
header = cd.getHeaderKey();
}
target.append("<th>");
// header either a link or not
if (table.isSortingEnabled() && cd.isSortingAllowed()) {
target.append("<a class='o_orderby' ");
ubu.buildHrefAndOnclick(target, ajaxEnabled,
new NameValuePair(Table.FORM_CMD, Table.COMMAND_SORTBYCOLUMN),
new NameValuePair(Table.FORM_PARAM, i)).append(">")
.append(header)
.append("</a>");
} else {
target.append(header);
}
target.append("</th>");
}
target.append("</tr></thead>");
}
private String renderMultiselectForm(final StringOutput target, final Component source, final URLBuilder ubu, final boolean iframePostEnabled) {
String formName = "tb_ms_" + source.hashCode();
target.append("<form method=\"post\" name=\"");
target.append(formName);
target.append("\" action=\"");
ubu.buildURI(target, null, null, iframePostEnabled ? AJAXFlags.MODE_TOBGIFRAME : AJAXFlags.MODE_NORMAL);
target.append("\" id=\"").append(formName).append("\"");
if (iframePostEnabled) {
target.append(" onsubmit=\"o_XHRSubmit('").append(formName).append("');\">");
} else {
target.append(" onsubmit=\"o_beforeserver();\">");
}
target.append("<input id=\"o_mai_").append(formName).append("\" type=\"hidden\" name=\"multi_action_identifier\" value=\"\"").append(" />");
return formName;
}
/**
* @param target
* @param ubu
* @param pageid
* @param maxpageid
*/
private void appendPageNumberLinks(StringOutput target, String formName, int pageid, int maxpageid, URLBuilder ubu) {
if (maxpageid < 12) {
addPageNumberLinksForSimpleCase(target, formName, pageid, maxpageid);
} else {
int powerOf10 = String.valueOf(maxpageid).length() - 1;
int maxStepSize = (int) Math.pow(10, powerOf10);
int stepSize = (int) Math.pow(10, String.valueOf(pageid).length() - 1);
boolean isStep = false;
int useEveryStep = 3;
int stepCnt = 0;
boolean isNear = false;
int nearleft = 5;
int nearright = 5;
if (pageid < nearleft) {
nearleft = pageid;
nearright += (nearright - nearleft);
} else if (pageid > (maxpageid - nearright)) {
nearright = maxpageid - pageid;
nearleft += (nearleft - nearright);
}
for (int i = 1; i <= maxpageid; i++) {
// adapt stepsize if needed
stepSize = adaptStepsizeIfNeeded(pageid, maxStepSize, stepSize, i);
isStep = ((i % stepSize) == 0);
if (isStep) {
stepCnt++;
isStep = isStep && (stepCnt % useEveryStep == 0);
}
isNear = (i > (pageid - nearleft) && i < (pageid + nearright));
if (i == 1 || i == maxpageid || isStep || isNear) {
appendPagenNumberLink(target, formName, pageid, i);
}
}
}
}
private void appendPagenNumberLink(StringOutput target, String formName, int pageid, int i) {
target.append("<li").append(" class='active'", pageid == i).append("><a href=\"#\" onclick=\"o_XHRSubmit('")
.append(formName).append("','").append(Table.FORM_CMD).append("','").append(Table.COMMAND_PAGEACTION)
.append("','").append(Table.FORM_PARAM).append("','").append(i).append("'); return false;\">")
.append(i).append("</a></li>");
}
private int adaptStepsizeIfNeeded(final int pageid, final int maxStepSize, final int stepSize, final int i) {
int newStepSize = stepSize;
if (i < pageid && stepSize > 1 && ((pageid - i) / stepSize == 0)) {
newStepSize = stepSize / 10;
} else if (i > pageid && stepSize < maxStepSize && ((i - pageid) / stepSize == 9)) {
newStepSize = stepSize * 10;
}
return newStepSize;
}
private void addPageNumberLinksForSimpleCase(StringOutput target, String formName, int pageid, int maxpageid) {
for (int i = 1; i <= maxpageid; i++) {
appendPagenNumberLink(target, formName, pageid, i);
}
}
}