package org.netxilia.server.js;
import static org.netxilia.server.js.NX.nx;
import static org.netxilia.server.jslib.NetxiliaGlobal.$;
import static org.stjs.javascript.Global.$array;
import static org.stjs.javascript.Global.$map;
import static org.stjs.javascript.Global.$or;
import static org.stjs.javascript.Global.alert;
import static org.stjs.javascript.Global.encodeURIComponent;
import static org.stjs.javascript.Global.parseInt;
import static org.stjs.javascript.Global.setTimeout;
import static org.stjs.javascript.Global.window;
import static org.stjs.javascript.JSStringAdapter.fromCharCode;
import static org.stjs.javascript.JSStringAdapter.match;
import static org.stjs.javascript.JSStringAdapter.replace;
import org.netxilia.server.js.TreeView.TreeNode;
import org.netxilia.server.js.data.ChartDescription;
import org.netxilia.server.js.data.EventData;
import org.netxilia.server.js.data.JsAreaReference;
import org.netxilia.server.js.data.JsCellReference;
import org.netxilia.server.js.data.NetxiliaEvent;
import org.netxilia.server.js.data.SheetDescription;
import org.netxilia.server.js.data.WindowIndex;
import org.netxilia.server.js.data.WindowInfo;
import org.netxilia.server.js.editors.EditingContext;
import org.netxilia.server.jslib.NetxiliaJQuery;
import org.netxilia.server.jslib.OpenFlashChart;
import org.stjs.javascript.Array;
import org.stjs.javascript.Date;
import org.stjs.javascript.Map;
import org.stjs.javascript.RegExp;
import org.stjs.javascript.dom.Element;
import org.stjs.javascript.dom.Input;
import org.stjs.javascript.dom.TableCell;
import org.stjs.javascript.dom.TableRow;
import org.stjs.javascript.functions.Callback0;
import org.stjs.javascript.functions.Callback1;
import org.stjs.javascript.functions.Callback2;
import org.stjs.javascript.functions.Callback3;
import org.stjs.javascript.functions.Function1;
import org.stjs.javascript.jquery.AjaxParams;
import org.stjs.javascript.jquery.Event;
import org.stjs.javascript.jquery.EventHandler;
import org.stjs.javascript.jquery.JQueryXHR;
import org.stjs.javascript.jquery.Position;
import org.stjs.javascript.jquery.plugins.DraggableOptions;
import org.stjs.javascript.jquery.plugins.DraggableUI;
import org.stjs.javascript.jquery.plugins.ResizableOptions;
import org.stjs.javascript.jquery.plugins.ResizeableUI;
import org.stjs.javascript.jquery.plugins.UIEventHandler;
public class Sheet {
public NetxiliaJQuery container; // this is the div in the main window
private NetxiliaJQuery cellContainer; // this is the div in the cell iframe
public boolean loaded = false; // becomes true when the iframe containing the cells finished loading
public NetxiliaJQuery table;
private NetxiliaJQuery fixedRowsTable;
public CellRange selection;
public NetxiliaJQuery selectionContent;
private NetxiliaJQuery selectedArea;
private TableCell selectionStartTd; // the TD where the user clicked when he starts selecting more cells
// sheet ;
private NetxiliaJQuery selector;
private NetxiliaJQuery replicator;
private boolean mouseDown = false;
private boolean replicatorDown = false;
private boolean hasFormulaCells = false;
private CellRange captureSelection;
private Map<Long, ConnectedWindow> windows; // these are the other windows editing the same sheet
private int resizedCol = -1;
private boolean resizingCol = false;
public EditingContext editingContext;
private TreeView treeView;
public String filter;
public int pageNo = 0;
private int pageCount = 1;
private int fixedRows = 0;
private int fixedCols = 0;
public SheetDescription desc;
private NetxiliaJQuery rows;
private Map<Integer, Integer> mapTrToRow;
private Map<Integer, Integer> mapRowToTr;
private int minRow = 0;
private int maxRow = 0;
// these are the indices of non-hidden TR
private int firstTr = 0;
private int lastTr = 0;
public Map<String, String> aliases;
private long lastChangeTime = 0;
public boolean waitForKeypress = false;
private NetxiliaJQuery aliasName;
private NetxiliaJQuery colAliasNames;
private NetxiliaJQuery aliasRef;
private NetxiliaJQuery formulaTip;
private boolean hasMarkedCells;
private Element focusElement;
public boolean filterFormula;
private Array<ChartDescription> charts;
private Array<String> spans;
private NetxiliaJQuery layout;
public Shortcuts shortcuts;
protected NetxiliaJQuery colResizer;
protected int resizerStart;
private NetxiliaJQuery nxtable;
private NetxiliaJQuery rowResizer;
public int columnCount() {
// return this.cols.length;
return this.colIndex($(this.rows.get(0)).children().size(), true);
}
/**
*
* @param row
* @return the rowIndex in the sheet's table by offseting with the hidden rows
*/
public Integer rowIndex(int row, boolean trToRow) {
Integer ret = trToRow ? this.mapTrToRow.$get(row) : this.mapRowToTr.$get(row);
// if (ret == null)
// ret = trToRow ? this.mapTrToRow[1] : 1;
return ret;
}
public int colIndex(int col, boolean tdToCol) {
return tdToCol ? col - 1 : col + 1;
}
/**
* rows and cols are 0-based
*/
public Cell cell(Object p1, Object p2) {
// TODO need to handle fixed rows and cells also
// this.rows, this.table contain only non-fixed rows
Integer c = (Integer) p2, r = (Integer) p1;
if (p2 == null) {
c = ((JsCellReference) p1).col;
r = ((JsCellReference) p1).row;
}
Integer trId = this.rowIndex(r, false);
if (trId == null) {
trId = this.fixedRows + 1;
}
// r = bind(r, 0, this.rows.length - 1);
Element tr = this.rows.get(trId);
TableCell td = $(tr).tdAtIndex(this.colIndex(c, false));
return new Cell(this, r, c, td);
}
private Cell cellFromTd(TableCell td) {
TableRow tr = (TableRow) td.parentNode;
Integer tdIndex = $(td).getNonColSpanIndex();
return new Cell(this, this.rowIndex(tr.rowIndex, true), this.colIndex(tdIndex, true), td);
}
private void showRows(boolean show) {
if (show) {
$(".collapsed", this.table).removeClass("collapsed");
} else {
$("tr", this.table).addClass("collapsed");
}
}
private void showRow(int row, boolean show) {
Integer r = this.rowIndex(row, false);// nth-child is 1-based - but it seems tha jquery does not take hidden !?
if (show) {
$("tr:nth(" + r + ")", this.table).removeClass("collapsed");
} else {
$("tr:nth(" + r + ")", this.table).addClass("collapsed");
}
}
private void setRowNums(int startRow, int startTr) {
int row = startRow;
for (int r = startTr; r < this.rows.size(); ++r, ++row) {
$(this.rows.get(r)).find("th").text(row + 1);
}
}
private void insertRow(int row) {
String s = "<tr>";
s += "<th>" + (row + 1) + "</th>";
int colCount = this.columnCount();
for (int i = 0; i < colCount; ++i) {
s += "<td></td>";
}
s += "</tr>";
int tr = this.rowIndex(row, false);
$(this.rows.get(tr)).before(s);
this.setRowNums(row + 1, tr);
this.initTable();
}
private void deleteRow(int row) {
int tr = this.rowIndex(row, false);// nth-child is 1-based - but it seems tha jquery does not take hidden !?
$(this.rows.get(tr)).remove();
this.setRowNums(row - 1, tr);
this.initTable();
}
private void insertColumn(int col) {
this.viewPage(this.pageNo, false);
int c = this.colIndex(col, false) + 1;// nth-child is 1-based
// !! because the first column in the fixedRowsTable has a TH not a column header, use -1 on indexes
this.rows.find("td:nth-child(" + c + ")").before("<td></td>");
this.fixedRowsTable.find("tr.labels th:nth-child(" + c + ")").before("<th style='width:80px'></th>");
this.fixedRowsTable.find("tr.aliases th:nth-child(" + c + ")").before("<th><input type='text' value=''></th>");
this.container.nxtable("refreshTotalWidth");
this.rebuildColumnHeaders(c - 1);
}
private void deleteColumn(int col) {
this.viewPage(this.pageNo, false);
int c = this.colIndex(col, false) + 1;// nth-child is 1-based
// !! because the first column in the fixedRowsTable has a TH not a column header, use -1 on indexes
NetxiliaJQuery $td = $(".cells .cw td:eq(" + c + ")", this.cellContainer);
this.container.nxtable("refreshTotalWidth");
$("tr td:nth-child(" + c + ")", this.table).remove();
$("tr th:nth-child(" + (c - 1) + ")", this.fixedRowsTable).remove();
this.rebuildColumnHeaders(c - 1);
}
private void resizeColumn(int col, int w) {
int tdIndex = this.colIndex(col, false);
NetxiliaJQuery $col = $("tr:first th:eq(" + tdIndex + ")", this.fixedRowsTable);
NetxiliaJQuery $td = $("tr.cw td:eq(" + (tdIndex - 1) + ")", this.table);
$td.width(w);
$col.width(w);
this.container.nxtable("refreshTotalWidth");
nx.app.onResizeColumns(this);
}
private void rebuildColumnHeaders(int start) {
for (int c = start; c < this.columnCount(); ++c) {
String label = this.columnLabel(this.colIndex(c - 1, true));
$("tr.labels th:nth-child(" + c + ")", this.fixedRowsTable).text(label);
$("tr.aliases th:nth-child(" + c + ") input", this.fixedRowsTable).attr("id", "alias-" + label);
}
this.aliasName = $("tr.aliases th.alias input", this.fixedRowsTable);
this.colAliasNames = $("tr.aliases th input", this.fixedRowsTable);
}
private String columnLabel(Integer col) {
// TODO convert to more than one-letter code
return col != null ? fromCharCode(String.class, 65 + col) : "";
}
private String rowLabel(Integer row) {
return row != null ? "" + (row + 1) : "";
}
public String cellRef(int row, int col, boolean fixedCol, boolean fixedRow, boolean addSheetName) {
return (addSheetName ? this.desc.name + "!" : "") + (fixedCol ? "$" : "") + this.columnLabel(col)
+ (fixedRow ? "$" : "") + (row + 1);
}
public String areaRef(int startRow, int startCol, int endRow, int endCol, boolean addSheetName) {
return (addSheetName ? this.desc.name + "!" : "") + this.columnLabel(startCol) + this.rowLabel(startRow) + ":"
+ this.columnLabel(endCol) + this.rowLabel(endRow);
}
private void setSpans(Array<String> areas, boolean merged) {
for (Integer a : areas) {
JsAreaReference area = nx.utils.parseAreaReference(areas.$get(a));
for (int r = area.topLeft.row; r <= area.bottomRight.row; ++r) {
for (int c = area.topLeft.col; c <= area.bottomRight.col; ++c) {
boolean topLeftCell = r == area.topLeft.row && c == area.topLeft.col;
int rowSpan = merged ? (topLeftCell ? area.bottomRight.row - area.topLeft.row + 1 : -1) : 1;
int colSpan = merged ? (topLeftCell ? area.bottomRight.col - area.topLeft.col + 1 : -1) : 1;
this.cell(r, c).span(rowSpan, colSpan);
}
}
}
}
/******* charts ***********/
private void chartMoved(Element chartDiv) {
NetxiliaJQuery $chart = $(chartDiv);
int id = parseInt($chart.attr("id").substring(5));
nx.resources.charts.move(this.desc.workbook, this.desc.name, id, parseInt($chart.css("left")),
parseInt($chart.css("top")), $chart.width(), $chart.height(), null, null);
}
private void chartRefresh(int id) {
NetxiliaJQuery swf = $("#chartFlash" + id, this.cellContainer);
((OpenFlashChart) swf.get(0)).reload(nx.app.desc.context + "/rest/charts/" + this.desc.workbook + "/"
+ this.desc.name + "/" + id);
}
private void chartDelete(int id) {
final Sheet that = this;
nx.resources.charts.del(this.desc.workbook, this.desc.name, id, new Callback1<Void>() {
@Override
public void $invoke(Void v) {
that.reload();
}
}, null);
}
private void chartSettings(int id) {
nx.app.setActiveSheet(this);
nx.app.dlgChart(id, this.charts.$get(id));
}
/*********** interaction *************/
private boolean editMode() {
return this.editingContext.editor != null;
}
private void moveMode(Shortcuts sh, String key, final Callback0 f) {
final Sheet that = this;
sh.add(key, new Function1<Event, Boolean>() {
@Override
public Boolean $invoke(Event p1) {
if (that.editMode() && !that.editingContext.defaultEditor && that.captureSelection == null) {
return true;
}
f.$invoke();
return false;
}
}, true);
}
private void moveSelection(int dc, int dr, boolean ignoreEditorDefault) {
// CHECK HERE
if (!ignoreEditorDefault && this.captureSelection != null) {
this.captureSelection.move(dc, dr);
this.editingContext.setCaptureSelection(this.captureSelection);
return;
}
if (!ignoreEditorDefault && !this.editingContext.defaultEditor) {
return;
}
this.beforeSelectionChanged();
this.editingContext.hide();
this.selection.move(dc, dr);
this.selectionChanged();
this.container.nxtable("makeVisible", this.rowIndex(this.selection.start.row, false),
this.colIndex(this.selection.start.col, false));
}
private void moveSelectionToLimits(boolean firstRow, boolean firstCol, boolean lastRow, boolean lastCol) {
this.editingContext.hide();
int r = firstRow ? this.minRow : this.selection.start.row;
r = lastRow ? this.maxRow : r;
int c = firstCol ? 0 : this.selection.start.col;
c = lastCol ? this.columnCount() - 1 : c;
this.selectionRange(r, c, r, c, false, false);
this.container.nxtable("makeVisible", this.rowIndex(this.selection.start.row, false),
this.colIndex(this.selection.start.col, false));
}
private int rowFromPosition(int y) {
for (int r = this.minRow + 1; r <= this.maxRow; ++r) {
int rt = $(this.rows.get(this.rowIndex(r, false))).offset().top;
if (rt > y) {
return r - 1;
}
}
return this.maxRow;
}
private void moveSelectionPage(String dir) {
NetxiliaJQuery cellsDiv = $(".cellsDiv", this.container);
Bounds div = cellsDiv.scrollBounds();
int r = this.rowFromPosition(dir == "down" ? (int) org.stjs.javascript.Math.min(div.b, this.table.height()
- div.h) : div.t - div.h);
int rt = $(this.rows.get(this.rowIndex(r, false))).offset().top;
this.selectionRange(r, this.selection.start.col, r, this.selection.start.col, false, false);
cellsDiv.scrollTop(rt);
}
public void selectionRange(int startRow, int startCol, int endRow, int endCol, boolean fullRow, boolean fullCol) {
this.beforeSelectionChanged();
this.selection.setRange(this.cell(startRow, startCol), this.cell(endRow, endCol), false, fullRow, fullCol);
this.selectionChanged();
}
private void selectionRangeTd(TableCell startTd, TableCell endTd, boolean withReplicator) {
if (this.captureSelection != null) {
this.captureSelection.setRange(startTd != null ? this.cellFromTd(startTd) : null, //
endTd != null ? this.cellFromTd(endTd) : null, false, false, false);
this.editingContext.setCaptureSelection(this.captureSelection);
return;
}
this.beforeSelectionChanged();
this.selection.setRange(startTd != null ? this.cellFromTd(startTd) : null, //
endTd != null ? this.cellFromTd(endTd) : null, withReplicator, false, false);
this.selectionChanged();
}
private void positionElementOnCell(NetxiliaJQuery elem, Cell cell) {
Position parentPos = this.table.parent().offset();
Position pos = cell.$td.offset();
pos.top -= parentPos.top;
pos.left -= parentPos.left;
int w = cell.$td.innerWidth(), h = cell.$td.innerHeight();
elem.css($map("top", pos.top, "left", pos.left, "width", w, "height", h));
}
private void placeSelectors() {
NetxiliaJQuery startCell = this.selection.start.$td;
Position parentPos = this.table.parent().offset();
parentPos.top -= this.table.parent().scrollTop();
parentPos.left -= this.table.parent().scrollLeft();
Position pos = startCell.offset();
pos.top -= parentPos.top;
pos.left -= parentPos.left;
int w = 60, h = 16;
// because of chrome that behaves strangely when cells are hidden
if (startCell.css("display") != "none") {
w = startCell.innerWidth();
h = startCell.innerHeight();
}
NetxiliaJQuery endCell = this.selection.end.$td;
Position epos = pos;
int ew = w, eh = h;
if (startCell != endCell) {
epos = endCell.offset();
epos.top -= parentPos.top;
epos.left -= parentPos.left;
if (endCell.css("display") != "none") {
ew = endCell.outerWidth();
eh = endCell.outerHeight();
}
}
this.selector.css($map("top", pos.top, "left", pos.left, "width", w, "height", h));
this.selectionContent.css($map("top", pos.top, "left", pos.left));
this.selectedArea.css($map("top", pos.top, "left", pos.left, "width", epos.left - pos.left + ew, "height",
epos.top - pos.top + eh));
if (this.selection.start.formula() != null) {
this.formulaTip.css($map("top", pos.top + 2 * h, "left", pos.left + 40));
}
if (this.selection.replicated) {
this.replicator.css($map("top", epos.top + eh, "left", epos.left + ew));
} else {
this.replicator.css($map("top", pos.top + h, "left", pos.left + w));
}
}
private void beforeSelectionChanged() {
if (this.selection.start != null) {
// value from editor -> cell
if (this.editingContext.hasValueChanged()) {
String value = this.editingContext.value();
if (this.editingContext.defaultEditor) {
// check for smart-edit
String crtFormula = this.selection.start.formula();
if (crtFormula != null) {
RegExp re = new RegExp("([\\(+\\-*%=\\/]+\\s*)\\(\\d+\\)", "g");
if (match(crtFormula, re) != null) {
value = replace(crtFormula, re, "$1(" + value + ")");
}
}
}
this.lastChangeTime = new Date().getTime();
nx.resources.cells.setValue(this.desc.workbook, this.selection.start.ref(true), value, null);
}
}
}
/**
* called anytime the selection changed. TODO use events
*/
private void selectionChanged() {
this.selectionContent.val(this.selection.editableValue());
this.editingContext.hide();
this.markFormulaCells(null);
if (this.selection.start.formula() != null) {
this.formulaTip.html(this.selection.start.formula());
this.formulaTip.show();
} else {
this.formulaTip.hide();
}
this.placeSelectors();
this.focusSelectionContent();
// select row & column headers
$(".sel", this.fixedRowsTable).removeClass("sel");
$(".sel", this.table).removeClass("sel");
if (!this.selection.fullCol) {
for (int r = this.selection.start.row; r <= this.selection.end.row; ++r) {
$("tr:nth(" + (this.rowIndex(r, false)) + ") th", this.table).addClass("sel");
}
}
if (!this.selection.fullRow) {
for (int c = this.selection.start.col; c <= this.selection.end.col; ++c) {
$("tr.labels th:nth(" + (this.colIndex(c, false)) + ")", this.fixedRowsTable).addClass("sel");
}
}
if (!nx.utils.isEmptyObject(this.windows)) {
nx.resources.windows.notifySelection(nx.app.windowId, this.selection.ref(true), null, null);
}
String ref = this.selection.ref(false);
this.aliasRef.text(ref);
this.aliasName.val($or(this.aliases.$get(ref), ""));
nx.app.menuStatus();
}
/**
* called to display the selection made in another window opened to the same sheet
*/
private void markSelection(String areaRef, WindowIndex windowId) {
ConnectedWindow s = this.windows.$get(windowId.id);
if (s == null) {
return;
}
JsAreaReference ref = nx.utils.parseAreaReference(areaRef);
if (ref == null || ref.topLeft == null) {
return;
}
this.positionElementOnCell(s.selector, this.cell(ref.topLeft, null));
}
/**
* display the editor for the current selection. and mark the areas in the formula
*/
private void showEditor() {
this.editingContext.edit(this.selection.start, null, null);
this.markFormulaCells(this.selection.start.formula());
}
private void toggleMarkArea(JsAreaReference ref, String css) {
this.markArea(this.hasMarkedCells ? null : ref, css);
}
private void markArea(JsAreaReference ref, String css) {
if (ref != null) {
if (ref.topLeft.sheet == null || ref.topLeft.sheet == this.desc.name) {
for (int r = ref.topLeft.row; r <= ref.bottomRight.row; ++r) {
for (int c = ref.topLeft.col; c <= ref.bottomRight.col; ++c) {
Cell cell = this.cell(r, c);
cell.$td.addClass(css);
}
}
}
this.hasMarkedCells = true;
} else {
this.table.find("td." + css).removeClass(css);
this.hasMarkedCells = false;
}
}
private void markFormulaCells(String formula) {
if (this.hasMarkedCells) {
this.markArea(null, "formula");
}
if (formula == null || formula.length() < 1 || formula.charAt(0) != '=') {
this.captureSelection = null;
return;
}
Array<JsAreaReference> refs = nx.utils.findReferencesInFormula(formula);
for (Integer i : refs) {
JsAreaReference ref = refs.$get(i);
this.markArea(ref, "formula");
}
}
private void addWindow(WindowInfo windowInfo) {
long wid = windowInfo.windowId.id;
ConnectedWindow s = new ConnectedWindow(wid, windowInfo.username, $("<div class='selector-other'></div>")
.appendTo(this.cellContainer));
this.windows.$put(s.id, s);
}
private void removeWindow(WindowInfo windowInfo) {
long wid = windowInfo.windowId.id;
ConnectedWindow s = this.windows.$get(wid);
if (s == null) {
return;
}
s.selector.remove();
this.windows.$delete(s.id);
}
private void clearCells() {
nx.resources.cells.setValue(this.desc.workbook, this.selection.ref(true), "", null);
}
private void cancelEdit() {
this.editingContext.hide();
this.markFormulaCells(null);
}
private void borders(Map<String, Array<String>> borderStyle) {
if (borderStyle == null) {
// clear all cells
nx.resources.cells.applyStyle(this.desc.workbook, this.areaRef(
(int) org.stjs.javascript.Math.max(this.selection.start.row - 1, 0),
(int) org.stjs.javascript.Math.max(this.selection.start.col - 1, 0), this.selection.end.row,
this.selection.end.col, true), "br bb bt bl", "clear", null);
return;
}
Array<CellWithStyle> updates = this.selection.borders(borderStyle);
for (int u : updates) {
nx.resources.cells.applyStyle(this.desc.workbook, updates.$get(u).ref, updates.$get(u).style, "add", null);
}
this.focusSelectionContent();
}
public void checkAutoInsertRow() {
if (nx.app.autoInsertRow) {
this.moveSelection(0, 1, true);
}
}
private void buildTreeView(boolean refresh) {
final Sheet controller = this;
if (refresh) {
$(".tree", this.table).removeClass("tree");
}
this.treeView = new TreeView();
for (int r = this.minRow; r <= this.maxRow; ++r) {
int level = -1;
for (int c = 0; c < this.columnCount(); ++c) {
String v = this.cell(r, c).valueAsString(null);
if (v != null && v.length() > 0) {
level = c;
break;
}
}
if (level >= 0) {
this.treeView.node(level, "" + r, null);
}
}
this.treeView.walk(new Callback1<TreeView.TreeNode>() {
@Override
public void $invoke(TreeNode n) {
if (n.children.$length() > 0) {
controller.cell(n.key, n.level).$td.addClass("tree");
}
}
}, null);
}
/**
* toggles to tree view
*/
public void toggleTreeView() {
if (this.treeView == null) {
this.buildTreeView(false);
} else {
// clear all nodes
this.showRows(true);
$(".tree", this.table).removeClass("tree");
this.treeView = null;
}
}
private void toggleTreeNode(TableCell td) {
final Sheet controller = this;
Cell cell = this.cellFromTd(td);
TreeView.TreeNode n = this.treeView.nodes.$get("" + cell.row);
final boolean expand = cell.$td.hasClass("collapsed");
if (expand) {
cell.$td.removeClass("collapsed");
} else {
cell.$td.addClass("collapsed");
}
this.treeView.walk(new Callback1<TreeView.TreeNode>() {
@Override
public void $invoke(TreeNode nc) {
controller.showRow(parseInt(nc.key), expand);
}
}, n);
}
/**
* toggle the filter-by-cell.
*/
public void toggleFilter(boolean useFormula) {
this.filterFormula = useFormula;
if (this.filter == null) {
this.filter = null;
} else {
this.filter = useFormula ? this.selection.start.formula() : "="
+ this.cellRef(0, this.selection.start.col, false, false, false) + "="
+ this.selection.start.absoluteRef(false);
}
this.reload();
}
/**
* display the column aliases in the input boxes
*/
private void displayAliases() {
// set column aliases
int colCount = this.columnCount();
for (int i = 0; i < colCount; ++i) {
String colLabel = this.columnLabel(i);
$("#alias-" + colLabel, this.fixedRowsTable).val($or(this.aliases.$get(colLabel + ":" + colLabel), ""));
}
}
/**
* events
*/
public void processEvents(Array<NetxiliaEvent> events) {
boolean refreshSelection = false;
Array<Integer> rowsToResize = $array();
long t1 = new Date().getTime();
// this id for cell event
for (int e : events) {
NetxiliaEvent ev = events.$get(e);
if (ev.type == "cellModified") {
for (int d : ev.data) {
EventData evd = ev.data.$get(d);
Cell cell = this.cell(evd.row, evd.column);
cell.valueAsString($or(evd.formattedValue, ""));
cell.setCss(evd.style);
cell.setValue(evd.value);
rowsToResize.push(this.rowIndex(cell.row, false));
}
} else if (ev.type == "rowInserted") {
this.insertRow(ev.row);
refreshSelection = true;
} else if (ev.type == "rowDeleted") {
this.deleteRow(ev.row);
refreshSelection = true;
} else if (ev.type == "columnInserted") {
this.insertColumn(ev.column);
refreshSelection = true;
} else if (ev.type == "columnDeleted") {
this.deleteColumn(ev.column);
refreshSelection = true;
} else if (ev.type == "columnModified") {
this.resizeColumn(ev.column, ev.width);
refreshSelection = true;
} else if (ev.type == "cellSelected") {
this.markSelection(ev.selectedArea, ev.windowInfo.windowId);
} else if (ev.type == "sheetModified") {
// this.reload();
this.aliases = (Map) $or(nx.utils.reverseMap(ev.aliases), $map());
this.displayAliases();
// set spans spans
Diff<String> diff = nx.utils.diff(this.spans, ev.spans);
this.setSpans(diff.deleted, false);
this.setSpans(diff.added, true);
this.spans = ev.spans;
}
}
if (refreshSelection) {
this.selection.refresh();
}
this.selectionContent.val(this.selection.editableValue());
if (nx.app.activeSheet == this) {
this.focusSelectionContent();
}
this.placeSelectors();
if (this.treeView != null) {
this.buildTreeView(true);
}
long t2 = new Date().getTime();
// console.info("event time: " + (t2-t1) + " start since lastChange:" + (t1 - this.lastChangeTime));
}
public void focusSelectionContent() {
this.selectionContent.focus();
this.selectionContent.select();
}
public void syncScroll(Sheet sheet) {
this.container.nxtable("scrollLeft", sheet.container.nxtable("scrollLeft"));
}
public void syncColumnSizes(Sheet sheet) {
int w = sheet.table.width();
this.table.width(w);
$(".cells .cw", this.cellContainer).html($(".cells .cw", sheet.cellContainer).html());
}
public void viewPage(int p, boolean showPageOnly) {
final Sheet that = this;
$(".pager table tbody tr.crt", this.container).removeClass("crt");
this.pageNo = p;
$(".pager table tbody tr:nth(" + this.pageNo + ")", this.container).addClass("crt");
if (!showPageOnly) {
String u = this.desc.context + "/rest/sheets/" + this.desc.workbook + "/" + this.desc.name + "?start="
+ (this.pageNo * nx.app.pageSize);
if (this.filter != null) {
u += "&filter=" + encodeURIComponent(this.filter);
}
final String uu = u;
$.ajax(new AjaxParams() {
{
url = uu;
success = new Callback3<String, String, JQueryXHR>() {
@Override
public void $invoke(String html, String status, JQueryXHR request) {
that.table.replaceWith(html);
that.initTable();
that.initSelection();
that.handleMouse(true);
}
};
error = new Callback3<JQueryXHR, String, String>() {
@Override
public void $invoke(JQueryXHR p1, String p2, String p3) {
alert("Could not change the page!");
}
};
}
});
}
}
private void buildPager() {
NetxiliaJQuery $pager = $(".pager", this.container);
if (this.pageCount <= 1) {
this.layout.threeColumn("right", 0);
return;
}
String content = "<table style='width:100%;'>";
content += "<thead><tr><th>ROWS</th></tr></thead>";
content += "<tbody>";
for (int p = 1; p <= this.pageCount; ++p) {
content += "<tr><td>";
content += (nx.app.pageSize * (p - 1) + 1);
content += "</td></tr>";
}
content += "</tbody>";
content += "</table>";
$pager.html(content);
this.layout.threeColumn("right", 60);
}
public void reload() {
this.viewPage(this.pageNo, false);
}
private void changeAlias(String oldAlias, String newAlias, String ref) {
oldAlias = $or(oldAlias, "");
newAlias = $or(newAlias, "");
if (oldAlias != newAlias) {
if (oldAlias != "") {
nx.resources.sheets.deleteAlias(this.desc.workbook, this.desc.name, oldAlias, null, null);
}
if (newAlias != "") {
nx.resources.sheets.setAlias(this.desc.workbook, this.desc.name, newAlias, ref, null, null);
}
}
}
/**
* handlers for mouse interaction
*/
private void handleMouse(boolean tableOnly) {
final Sheet that = this;
if (tableOnly) {
$(this.table).mousedown(new EventHandler() {
@Override
public boolean onEvent(Event ev, Element THIS) {
nx.app.setActiveSheet(that);
// trigger first the blur event that is canceled by "return false" from this function
if (that.focusElement != null) {
$(that.focusElement).blur();
}
NetxiliaJQuery $td = null;
if (ev.target.tagName.toLowerCase() == "div" && ev.target.className == "merge") {
$td = $(ev.target).parent();
} else if (ev.target.tagName.toLowerCase() == "td") {
$td = $(ev.target);
} else if (ev.target.tagName.toLowerCase() == "th") {
// row headers
NetxiliaJQuery $th = $(ev.target);
int r = that.rowIndex(((TableRow) $th.get(0).parentNode).rowIndex, true);
that.selectionRange(r, 0, r, that.columnCount() - 1, true, false);
return false;
} else if (ev.target.className == "ck-format") {
$td = $(ev.target).parent();
boolean ck = !((Input) ev.target).checked;// the checked flag will change after this method
nx.resources.cells.setValue(that.desc.workbook,
that.cellFromTd((TableCell) $td.get(0)).ref(true), ck ? "true" : "false", null);
return true;
}
if ($td != null) {
if (that.treeView != null && $td.hasClass("tree") && ev.pageX - $td.offset().left <= 10) {
that.toggleTreeNode((TableCell) ev.target);
return false;
}
that.mouseDown = true;
if (ev.shiftKey) {
that.selectionRangeTd(that.selectionStartTd, (TableCell) $td.get(0), false);
} else {
that.selectionStartTd = (TableCell) $td.get(0);
that.selectionRangeTd((TableCell) $td.get(0), null, false);
}
if ($.browser.safari) {
// just to take the focus
// that.editingContext.showDefaultEditor(that.selection.start);
// that.editingContext.hide();
}
}
return false;
}
});
$(this.table).mousemove(new EventHandler() {
@Override
public boolean onEvent(Event ev, Element THIS) {
if (ev.target.tagName.toLowerCase() == "td") {
if (that.replicatorDown) {
that.selectionRangeTd(that.selectionStartTd, (TableCell) ev.target, true);
} else if (that.mouseDown) {
that.selectionRangeTd(that.selectionStartTd, (TableCell) ev.target, false);
}
}
return false;
}
});
return;
}
this.selector.mousedown(new EventHandler() {
@Override
public boolean onEvent(Event ev, Element THIS) {
nx.app.setActiveSheet(that);
// trigger first the blur event that is canceled by "return false" from this function
if (that.focusElement != null) {
$(that.focusElement).blur();
}
that.selectionRange(that.selection.start.row, that.selection.start.col, that.selection.end.row,
that.selection.end.col, false, false);
return false;
}
});
/** double click puts up directly the edit text */
this.selector.dblclick(new EventHandler() {
@Override
public boolean onEvent(Event ev, Element THIS) {
that.showEditor();
return false;
}
});
// column headers
$("tr.labels th", this.fixedRowsTable).live("mousemove", new EventHandler() {
@Override
public boolean onEvent(Event ev, Element THIS) {
if (that.resizingCol) {
return false;
}
NetxiliaJQuery $th = $(THIS);
Position pos = $th.offset();
Position parentPos = that.colResizer.parent().offset();
int w = $th.outerWidth();
int d = 0;
if (Math.abs(pos.left - ev.pageX) < 20) {
d = 0;
} else if (Math.abs(pos.left + w - ev.pageX) < 20) {
d = 1;
} else {
that.colResizer.hide();
return false;
}
int h = $th.outerHeight();
// this is the column that has its right edge moving
that.resizedCol = ((TableCell) THIS).cellIndex - 2 + d;
that.colResizer.css($map("left", pos.left + d * w - 5 - parentPos.left, "height", h));
// that.colResizer.draggable('option', 'containment', [0, pos.top - 1, 10000, pos.top + h + 1]);
that.colResizer.show();
return false;
}
}).live("mousedown", new EventHandler() {
@Override
public boolean onEvent(Event ev, Element THIS) {
nx.app.setActiveSheet(that);
// TODO should use full column selectors. ex: C:C
int c = that.colIndex(((TableCell) THIS).cellIndex, true);
that.selectionRange(that.minRow, c, that.maxRow, c, false, true);
return false;
}
});
this.colResizer.bind("dragstart", new EventHandler() {
@Override
public boolean onEvent(Event ev, Element THIS) {
that.resizingCol = true;
that.resizerStart = ev.pageX;
return false;
}
});
this.colResizer.bind("dragstop", new EventHandler() {
@Override
public boolean onEvent(Event ev, Element THIS) {
int cw = $(".cw td:eq(" + that.resizedCol + ")", that.table).width();
nx.resources.columns.modify(that.desc.workbook, that.desc.name, that.resizedCol, cw
+ (ev.pageX - that.resizerStart), null);
that.placeSelectors();
that.resizedCol = -1;
that.resizingCol = false;
that.colResizer.hide();
return false;
}
});
this.selector.mousedown(new EventHandler() {
@Override
public boolean onEvent(Event ev, Element THIS) {
that.mouseDown = true;
return false;
}
});
this.replicator.mousedown(new EventHandler() {
@Override
public boolean onEvent(Event ev, Element THIS) {
that.replicatorDown = true;
return false;
}
});
$(this.cellContainer).mouseup(new EventHandler() {
@Override
public boolean onEvent(Event ev, Element THIS) {
if (that.replicatorDown) {
nx.app.replicate();
}
that.mouseDown = false;
that.replicatorDown = false;
return false;
}
});
$(".pager", this.container).click(new EventHandler() {
@Override
public boolean onEvent(Event ev, Element THIS) {
if (ev.target.tagName.toLowerCase() == "td") {
that.viewPage(((TableRow) ev.target.parentNode).rowIndex - 1, false);
}
return false;
}
});
this.aliasName.add(this.colAliasNames).focus(new EventHandler() {
@Override
public boolean onEvent(Event ev, Element THIS) {
that.focusElement = THIS;
return false;
}
});
this.aliasName.blur(new EventHandler() {
@Override
public boolean onEvent(Event ev, Element THIS) {
that.focusElement = null;
String oldAlias = that.aliases.$get(that.aliasRef.text());
String newAlias = (String) $(THIS).val();
that.changeAlias(oldAlias, newAlias, that.aliasRef.text());
that.focusSelectionContent();
return false;
}
});
this.colAliasNames.blur(new EventHandler() {
@Override
public boolean onEvent(Event ev, Element THIS) {
that.focusElement = null;
String colLabel = THIS.id.substring("alias-".length());
String ref = colLabel + ":" + colLabel;
String oldAlias = that.aliases.$get(ref);
String newAlias = (String) $(THIS).val();
that.changeAlias(oldAlias, newAlias, ref);
that.focusSelectionContent();
return false;
}
});
this.container.bind("nxtablebodyScroll", new EventHandler() {
@Override
public boolean onEvent(Event ev, Element THIS) {
that.placeSelectors();
nx.app.onScroll(that);
return false;
}
});
}
/**
* handlers for keyboard interaction
*/
private void handleKeyboard() {
final Sheet controller = this;
Shortcuts sh = new Shortcuts();
sh.addSimple("ctrl+alt+S", new Callback0() {
@Override
public void $invoke() {
nx.app.sort();
}
});
sh.addPropagate("ctrl+c", new Callback0() {
@Override
public void $invoke() {
nx.app.cbCopy(false);
}
});
sh.addPropagate("ctrl+x", new Callback0() {
@Override
public void $invoke() {
nx.app.cbCut(false);
}
});
sh.addPropagate("ctrl+v", new Callback0() {
@Override
public void $invoke() {
setTimeout(new Callback0() {
public void $invoke() {
nx.app.cbPaste(false);
}
}, 50);
}
});
sh.addSimple("ctrl+z", new Callback0() {
@Override
public void $invoke() {
nx.app.undo();
}
});
sh.addSimple("ctrl+y", new Callback0() {
@Override
public void $invoke() {
nx.app.redo();
}
});
// cell movements
this.moveMode(sh, "up", new Callback0() {
@Override
public void $invoke() {
controller.moveSelection(0, -1, false);
}
});
this.moveMode(sh, "down", new Callback0() {
@Override
public void $invoke() {
controller.moveSelection(0, 1, false);
}
});
this.moveMode(sh, "right", new Callback0() {
@Override
public void $invoke() {
controller.moveSelection(1, 0, false);
}
});
this.moveMode(sh, "left", new Callback0() {
@Override
public void $invoke() {
controller.moveSelection(-1, 0, false);
}
});
this.moveMode(sh, "home", new Callback0() {
@Override
public void $invoke() {
controller.moveSelectionToLimits(false, true, false, false);
}
});
this.moveMode(sh, "end", new Callback0() {
@Override
public void $invoke() {
controller.moveSelectionToLimits(false, false, false, true);
}
});
this.moveMode(sh, "ctrl+home", new Callback0() {
@Override
public void $invoke() {
controller.moveSelectionToLimits(true, true, false, false);
}
});
this.moveMode(sh, "ctrl+end", new Callback0() {
@Override
public void $invoke() {
controller.moveSelectionToLimits(false, true, true, false);
}
});
this.moveMode(sh, "pagedown", new Callback0() {
@Override
public void $invoke() {
controller.moveSelectionPage("down");
}
});
this.moveMode(sh, "pageup", new Callback0() {
@Override
public void $invoke() {
controller.moveSelectionPage("up");
}
});
sh.addSimple("enter", new Callback0() {
@Override
public void $invoke() {
if (nx.app.autoInsertRow) {
nx.app.insertRow(true);
} else {
controller.moveSelection(0, 1, true);
}
}
});// same as down
sh.addSimple("tab", new Callback0() {
@Override
public void $invoke() {
controller.moveSelection(1, 0, true);
}
});// moves right
this.moveMode(sh, "delete", new Callback0() {
@Override
public void $invoke() {
controller.clearCells();
}
});
sh.addSimple("escape", new Callback0() {
@Override
public void $invoke() {
controller.cancelEdit();
}
});
sh.addSimple("f2", new Callback0() {
@Override
public void $invoke() {
controller.selector.dblclick();
}
});
sh.addSimple("f7", new Callback0() {
@Override
public void $invoke() {
controller.toggleFilter(false);
}
});
sh.addSimple("ctrl+f7", new Callback0() {
@Override
public void $invoke() {
controller.toggleFilter(true);
}
});
sh.addSimple("f5", new Callback0() {
@Override
public void $invoke() {
window.location.reload();
}
});
sh.addDefault(new Function1<Event, Boolean>() {
@Override
public Boolean $invoke(Event ev) {
if (!controller.editMode()) {
if (ev.which < 32 || ev.ctrlKey) {
return true;
}
controller.waitForKeypress = true;
return false;
}
return true;
}
}, true);
this.shortcuts = sh;
}
/**
* initializing the sheet
*/
public Sheet init(SheetDescription desc, NetxiliaJQuery container) {
this.container = container;
this.desc = desc;
return this;
}
/**
* called when the sheet's frame was loaded. it calculates the size of the sheet's table container
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
private void frameLoaded(NetxiliaJQuery frmWindow, int pageCount, SheetDescription sheetData) {
final Sheet that = this;
this.layout = $(".threeColumnFixed", this.container).threeColumn();
this.pageNo = 0;
this.pageCount = pageCount;
this.buildPager();
this.viewPage(0, true);
// alias on the server are alias -> ref. on the client side are store ref->alias
if (sheetData != null) {
this.aliases = $or((Map) nx.utils.reverseMap(sheetData.aliases), $map());
this.charts = sheetData.charts;
this.spans = sheetData.spans;
}
this.windows = $map();
this.nxtable = this.container.nxtable();
this.cellContainer = $(".cellsDiv", this.container);
this.fixedRowsTable = $(".fixedRows", this.container);
this.selector = $("<div id='selector'></div>").appendTo(this.cellContainer);
this.formulaTip = $("<div id='formulaTip'></div>").appendTo(this.cellContainer);
this.selectedArea = $("<div id='selectedArea'></div>").appendTo(this.cellContainer);
this.replicator = $("<div id='replicator'></div>").appendTo(this.cellContainer);
this.rowResizer = $("<div id='rowResizer'></div>").appendTo(this.container);
this.colResizer = $("<div id='colResizer'></div>").appendTo(this.fixedRowsTable.parent());
this.colResizer.draggable(new DraggableOptions<NetxiliaJQuery>() {
{
axis = "x";
}
});
this.selectionContent = $("<textarea id='selectionContent' autocapitalize='off'></textarea>").appendTo(
this.cellContainer);
this.aliasRef = $("tr.labels th.ref", this.fixedRowsTable);
this.aliasName = $("tr.aliases th.alias input", this.fixedRowsTable);
this.colAliasNames = $("tr.aliases th input", this.fixedRowsTable).not("th.alias input");
this.initTable();
this.editingContext = new EditingContext(this);
this.editingContext.valueChanged = new Callback1<String>() {
@Override
public void $invoke(String value) {
if (that.captureSelection == null) {
that.captureSelection = new CellRange(that);
that.captureSelection.setRange(that.selection.start, null, false, false, false);
}
that.markFormulaCells(value);
}
};
this.handleMouse(false);
this.handleMouse(true);
this.handleKeyboard();
$(".chart", this.container).draggable(new DraggableOptions<NetxiliaJQuery>() {
{
stop = new UIEventHandler<DraggableUI<NetxiliaJQuery>>() {
@Override
public boolean onEvent(Event ev, DraggableUI<NetxiliaJQuery> ui, Element THIS) {
that.chartMoved(THIS);
return false;
}
};
}
});
$(".chart", this.container).resizable(new ResizableOptions<NetxiliaJQuery>() {
{
stop = new UIEventHandler<ResizeableUI<NetxiliaJQuery>>() {
@Override
public boolean onEvent(Event ev, ResizeableUI<NetxiliaJQuery> ui, Element THIS) {
that.chartMoved(THIS);
return false;
}
};
}
});
this.initSelection();
this.loaded = true;
// this.displayAliases();
nx.app.onFrameLoaded(this);
}
private void initTable() {
final Sheet that = this;
this.table = $(".cells", this.cellContainer);
this.rows = $("tbody tr", this.table);
this.mapTrToRow = $map();
this.mapRowToTr = $map();
this.minRow = 1000000;
this.maxRow = 0;
this.firstTr = 1;
this.lastTr = this.rows.size() - 1;
$("tbody th", this.table).each(new Callback2<Integer, Element>() {
@Override
public void $invoke(Integer idx, Element elm) {
if (idx == 0) {
return;
}
int rowId = parseInt($(this).text()) - 1;
that.mapTrToRow.$put(idx + that.firstTr - 1, rowId);
that.mapRowToTr.$put(rowId, idx + that.firstTr - 1);
that.maxRow = Math.max(that.maxRow, rowId);
that.minRow = Math.min(that.minRow, rowId);
}
});
}
private void initSelection() {
if (this.columnCount() > 0) {
if (this.selection != null) {
// already a selection
this.selectionRange(this.selection.start.row, this.selection.start.col, this.selection.end.row,
this.selection.end.col, false, false);
} else {
this.selection = new CellRange(this);
this.selectionRangeTd(this.cell(this.rowIndex(1, true), this.colIndex(1, true)).td, null, false);
}
}
}
}