package org.netxilia.server.js.plugins; import static org.stjs.javascript.Global.$array; import static org.stjs.javascript.Global.$map; import static org.stjs.javascript.Global.$object; import static org.stjs.javascript.Global.$properties; import static org.stjs.javascript.Global.parseInt; import static org.stjs.javascript.jquery.GlobalJQuery.$; import org.netxilia.server.js.Bounds; import org.netxilia.server.jslib.BoundsPlugin; import org.netxilia.server.jslib.TDJQueryHelpers; import org.stjs.javascript.Array; import org.stjs.javascript.Map; import org.stjs.javascript.dom.Element; import org.stjs.javascript.functions.Callback2; import org.stjs.javascript.jquery.JQueryAndPlugins; import org.stjs.javascript.jquery.impl.JQueryPlugin; /** * This plugin manages the horizontal and vertical scroll of a table with fixed columns and rows. Because the browser do * not offer this possibility directly, the techniques involve the usage of separate tables for the columns and rows and * synchronize then the widths and heights. Using only this technique, when having many rows the synchronization of row * heights is time consuming. This plugin uses a mixed technique: for fixed rows it uses a separate table and for fixed * columns uses a technique inspired by Google Spreadsheet: the fixed columns are in the main table but the non-fixed * columns are displayed or hidden as the user scrolls. The performance problem is fixed but this adds another * complication when dealing with colspans (merged cells). */ public class NXTable<FullJQuery extends JQueryAndPlugins<?>> extends JQueryPlugin { private NXTableOptions options; private FullJQuery fixedRowsTable; private FullJQuery cellsDiv; private FullJQuery table; private FullJQuery rows; private FullJQuery horizScroll; private int fixedCols; private int firstVisibleCol; private FullJQuery element; protected Array<Integer> columnWidths; @Override @SuppressWarnings({ "unchecked", "rawtypes" }) protected void _init() { final NXTable<FullJQuery> that = this; NXTableOptions o = (NXTableOptions) $object($.extend($map(), (Map) $properties(defaults), (Map) $properties(this.options))); this.fixedRowsTable = (FullJQuery) $(o.fixedRowsDivSelector, this.element).find("table"); this.cellsDiv = (FullJQuery) $(o.cellsDivSelector, this.element); this.table = (FullJQuery) $(o.cellsDivSelector, this.element).find("table"); this.rows = (FullJQuery) this.table.find("tr"); this.horizScroll = (FullJQuery) $(o.horizontalScrollSelector, this.element); this.fixedCols = 1; this.firstVisibleCol = this.fixedCols; /* * this.horizScroll.scroll(new EventHandler() { * * @Override public boolean onEvent(Event ev, Element THIS) { int x = $(THIS).scrollLeft(); that.scrollLeft(x); * return false; } }); */ } /** * set the col column's display for all the rows */ @SuppressWarnings("unchecked") private void _setColDisplay(final int col, final String display) { String crtDisplay = (String) $(((TDJQueryHelpers) $(this.rows.get(0))).tdAtIndex(col)).css("display"); if (crtDisplay == display) { return; } final NXTable<FullJQuery> that = this; this.rows.each(new Callback2<Integer, Element>() { @Override public void $invoke(Integer idx, Element THIS) { FullJQuery $td = (FullJQuery) $(((TDJQueryHelpers) $(THIS)).tdAtIndex(col)); if ($td.attr("colSpan") == "1" && !$td.hasClass(".mergeEmptyCell")) { // regular cells $td.css("display", display); } else { // merged cells if (display == "none") { // hide $td.before("<td class='mergeEmptyCell' style='display:none'></td>"); } else { // show // find the first td with the merge div (with the colspan) FullJQuery tdMerged = (FullJQuery) $td.next(":has(.merge)"); $td.remove(); $td = tdMerged; } int dir = (display == "none" ? -1 : 1); FullJQuery $divMerge = (FullJQuery) $td.find(".merge"); $divMerge.css("left", parseInt($divMerge.css("left")) + dir * that.columnWidths.$get(col)); $divMerge.width($divMerge.width() - dir * that.columnWidths.$get(col)); ((TDJQueryHelpers) $td).colSpan(parseInt($td.attr("colSpan")) + dir); } } }); } protected int columnCount() { return $("tr:first", this.table).children().size(); } @SuppressWarnings("unchecked") private void _buildColumnWidths() { if (this.columnWidths != null) { return; } this.columnWidths = $array(); for (int c = 0; c < this.columnCount(); ++c) { FullJQuery $td; if (c < this.fixedCols) { $td = (FullJQuery) $(".cw th:eq(" + c + ")", this.table); } else { int tdId = c - this.fixedCols; $td = (FullJQuery) $(".cw td:eq(" + tdId + ")", this.table); } int tdw = $td.get(0).offsetWidth;// $td.width(); this.columnWidths.push(tdw); } } private int _columnLeft(int col) { int w = 0; this._buildColumnWidths(); for (int c = 0; c < col; ++c) { w += this.columnWidths.$get(c); } return w; } /** * scroll horizontally and vertically to make sure the given cell is visible. row, col are 0-based and take into * account row and column headers */ protected void makeVisible(int row, int col) { // scroll if necessary Element cell = ((TDJQueryHelpers) $(this.rows.get(row))).tdAtIndex(col); Bounds sel = ((BoundsPlugin) $(cell)).bounds("parent"); Bounds div = ((BoundsPlugin) this.cellsDiv).scrollBounds(); if (sel.b + 10 >= div.b) { this.cellsDiv.scrollTop(sel.b + 10 - div.h); } sel.l = this._columnLeft(col) - this._columnLeft(1); // the fixed columns don't scroll sel.r = sel.l + this.columnWidths.$get(col); if (sel.r + 10 >= div.r || col < this.firstVisibleCol) { this.scrollLeft(sel.l); } } public int scrollLeft(Integer x) { if (x == null) { return this.horizScroll.scrollLeft(); } this.horizScroll.scrollLeft(x); // let the scroll bar manage min and amx int rx = this.horizScroll.scrollLeft(); // TODO optimize this this._buildColumnWidths(); int fullWidth = this.horizScroll.find("div").width(); int tw = 0; this.firstVisibleCol = this.fixedCols; for (int c = this.fixedCols; c < columnCount(); ++c) { int tdw = this.columnWidths.$get(c); tw += tdw; if (tw - tdw / 2 >= rx) { this.firstVisibleCol = c; break; } fullWidth -= tdw; } this.table.width(fullWidth); this.fixedRowsTable.width(fullWidth); // now hide cols on the left of the given position for (int c = this.fixedCols; c < this.columnCount(); ++c) { int tdId = c - 1; String display = c < this.firstVisibleCol ? "none" : ""; String tdi = "td:eq(" + tdId + ")"; String thi = "th:eq(" + (tdId + 1) + ")"; if (c >= this.firstVisibleCol && rows.find(tdi).css("display") != "none") { break; } _setColDisplay(c, display); // fixed rows table does not exists for summary sheet! fixedRowsTable.find("tr " + thi).css("display", display); } _trigger("bodyScroll"); return x; } protected void refreshTotalWidth() { int fullWidth = 0; this.columnWidths = null; this._buildColumnWidths(); int hiddenColsWidth = 0; // extract the hidden columns for (int c = this.fixedCols; c < this.columnCount(); ++c) { int tdw = this.columnWidths.$get(c); if (c < this.firstVisibleCol) { hiddenColsWidth += tdw; } fullWidth += tdw; } this.horizScroll.width(fullWidth - hiddenColsWidth); this.table.width(fullWidth - hiddenColsWidth); this.fixedRowsTable.width(fullWidth); } protected int totalWidth() { return this.horizScroll.width(); } protected int fixedColsWidth() { int w = 0; for (int c = 0; c < this.fixedCols; ++c) { w += this.columnWidths.$get(c); } return w; } @SuppressWarnings("unchecked") protected void synchronize(FullJQuery otherNxTable) { NXTable<FullJQuery> other = (NXTable<FullJQuery>) otherNxTable.data("nxtable"); this.horizScroll = other.horizScroll; int w = other.table.width(); this.table.width(w); $(".cw", this.table).html($(".cw", other.table).html()); } private final static NXTableOptions defaults = new NXTableOptions() { { cellsDivSelector = ".cellsDiv"; fixedRowsDivSelector = ".fixedRowsDiv"; horizontalScrollSelector = ".horizSheetScroll"; } }; public static void main(String[] args) { $.widget("nx.nxtable", new NXTable<JQueryAndPlugins<?>>()); // $.extend( // $.nx.splitter, // $map("version", "1.0", "getter", $array("scrollLeft", "totalWidth", "fixedColsWidth"), "defaults", // defaults)); } }