ethercalc
Version:
Multi-User Spreadsheet Server
1,366 lines (1,085 loc) • 231 kB
JavaScript
//
// SocialCalcTableEditor
//
/*
// The code module of the SocialCalc package that displays a scrolling grid with panes
// and handles keyboard and mouse I/O.
//
// (c) Copyright 2008, 2009, 2010 Socialtext, Inc.
// All Rights Reserved.
//
*/
/*
LEGAL NOTICES REQUIRED BY THE COMMON PUBLIC ATTRIBUTION LICENSE:
EXHIBIT A. Common Public Attribution License Version 1.0.
The contents of this file are subject to the Common Public Attribution License Version 1.0 (the
"License"); you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://socialcalc.org. The License is based on the Mozilla Public License Version 1.1 but
Sections 14 and 15 have been added to cover use of software over a computer network and provide for
limited attribution for the Original Developer. In addition, Exhibit A has been modified to be
consistent with Exhibit B.
Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
KIND, either express or implied. See the License for the specific language governing rights and
limitations under the License.
The Original Code is SocialCalc JavaScript TableEditor.
The Original Developer is the Initial Developer.
The Initial Developer of the Original Code is Socialtext, Inc. All portions of the code written by
Socialtext, Inc., are Copyright (c) Socialtext, Inc. All Rights Reserved.
Contributor: Dan Bricklin.
EXHIBIT B. Attribution Information
When the TableEditor is producing and/or controlling the display the Graphic Image must be
displayed on the screen visible to the user in a manner comparable to that in the
Original Code. The Attribution Phrase must be displayed as a "tooltip" or "hover-text" for
that image. The image must be linked to the Attribution URL so as to access that page
when clicked. If the user interface includes a prominent "about" display which includes
factual prominent attribution in a form similar to that in the "about" display included
with the Original Code, including Socialtext copyright notices and URLs, then the image
need not be linked to the Attribution URL but the "tool-tip" is still required.
Attribution Copyright Notice:
Copyright (C) 2010 Socialtext, Inc.
All Rights Reserved.
Attribution Phrase (not exceeding 10 words): SocialCalc
Attribution URL: http://www.socialcalc.org/xoattrib
Graphic Image: The contents of the sc-logo.gif file in the Original Code or
a suitable replacement from http://www.socialcalc.org/licenses specified as
being for SocialCalc.
Display of Attribution Information is required in Larger Works which are defined
in the CPAL as a work which combines Covered Code or portions thereof with code
not governed by the terms of the CPAL.
*/
//
// Some of the other files in the SocialCalc package are licensed under
// different licenses. Please note the licenses of the modules you use.
//
// Code History:
//
// Initially coded by Dan Bricklin of Software Garden, Inc., for Socialtext, Inc.
// Based in part on the SocialCalc 1.1.0 code written in Perl.
// The SocialCalc 1.1.0 code was:
// Portions (c) Copyright 2005, 2006, 2007 Software Garden, Inc.
// All Rights Reserved.
// Portions (c) Copyright 2007 Socialtext, Inc.
// All Rights Reserved.
// The Perl SocialCalc started as modifications to the wikiCalc(R) program, version 1.0.
// wikiCalc 1.0 was written by Software Garden, Inc.
// Unless otherwise specified, referring to "SocialCalc" in comments refers to this
// JavaScript version of the code, not the SocialCalc Perl code.
//
/*
See the comments in the main SocialCalc code module file of the SocialCalc package.
*/
var SocialCalc;
if (!SocialCalc) { // created here, too, in case load order is wrong, but main routines are required
SocialCalc = {};
}
// *************************************
//
// Table Editor class:
//
// *************************************
// Constructor:
SocialCalc.TableEditor = function(context) {
var scc = SocialCalc.Constants;
// Properties:
this.context = context; // editing context
this.toplevel = null; // top level HTML element for this table editor
this.fullgrid = null; // rendered editing context
this.noEdit = false; // if true, disable all edit UI and make read-only
this.width = null;
this.tablewidth = null;
this.height = null;
this.tableheight = null;
this.inputBox = null;
this.inputEcho = null;
this.verticaltablecontrol = null;
this.horizontaltablecontrol = null;
this.logo = null;
this.cellhandles = null;
// Dynamic properties:
this.timeout = null; // if non-null, timer id for position calculations
this.busy = false; // true when executing command, calculating, etc.
this.ensureecell = false; // if true, ensure ecell is visible after timeout
this.deferredCommands = []; // commands to execute after busy, in form: {cmdstr: "cmds", saveundo: t/f}
this.gridposition = null; // screen coords of full grid
this.headposition = null; // screen coords of upper left of grid within header rows
this.firstscrollingrow = null; // row number of top row in last (the scrolling) pane
this.firstscrollingrowtop = null; // position of top row in last (the scrolling) pane
this.lastnonscrollingrow = null; // row number of last displayed row in last non-scrolling
// pane, or zero (for thumb position calculations)
this.lastvisiblerow = null; // used for paging down
this.firstscrollingcol = null; // column number of top col in last (the scrolling) pane
this.firstscrollingcolleft = null; // position of top col in last (the scrolling) pane
this.lastnonscrollingcol = null; // col number of last displayed column in last non-scrolling
// pane, or zero (for thumb position calculations)
this.lastvisiblecol = null; // used for paging right
this.rowpositions = []; // screen positions of the top of some rows
this.colpositions = []; // screen positions of the left side of some rows
this.rowheight = []; // size in pixels of each row when last checked, or null/undefined, for page up
this.colwidth = []; // size in pixels of each column when last checked, or null/undefined, for page left
this.ecell = null; // either null or {coord: c, row: r, col: c}
this.state = "start"; // the keyboard states: see EditorProcessKey
this.workingvalues = {}; // values used during keyboard editing, etc.
// Constants:
this.imageprefix = scc.defaultImagePrefix; // URL prefix for images (e.g., "/images/sc")
this.idPrefix = scc.defaultTableEditorIDPrefix;
this.pageUpDnAmount = scc.defaultPageUpDnAmount; // number of rows to move cursor on PgUp/PgDn keys (numeric)
// Callbacks
// recalcFunction: if present, function(editor) {...}, called to do a recalc
// Default (sheet.RecalcSheet) does all the right stuff.
this.recalcFunction = function(editor) {
if (editor.context.sheetobj.RecalcSheet) {
editor.context.sheetobj.RecalcSheet(SocialCalc.EditorSheetStatusCallback, editor);
}
else return null;
};
// ctrlkeyFunction: if present, function(editor, charname) {...}, called to handle ctrl-V, etc., at top level
// Returns true (pass through for continued processing) or false (stop processing this key).
this.ctrlkeyFunction = function(editor, charname) {
var ta, cell, position, cmd, sel, cliptext;
switch (charname) {
case "[ctrl-c]":
case "[ctrl-x]":
ta = editor.pasteTextarea;
ta.value = "";
cell=SocialCalc.GetEditorCellElement(editor, editor.ecell.row, editor.ecell.col);
if (cell) {
position = SocialCalc.GetElementPosition(cell.element);
ta.style.left = (position.left-1)+"px";
ta.style.top = (position.top-1)+"px";
}
if (editor.range.hasrange) {
sel = SocialCalc.crToCoord(editor.range.left, editor.range.top)+
":"+SocialCalc.crToCoord(editor.range.right, editor.range.bottom);
}
else {
sel = editor.ecell.coord;
}
// get what to copy to clipboard
cliptext = SocialCalc.ConvertSaveToOtherFormat(SocialCalc.CreateSheetSave(editor.context.sheetobj, sel), "tab");
if (charname == "[ctrl-c]" || editor.noEdit || editor.ECellReadonly()) { // if copy or cut but in no edit
cmd = "copy "+sel+" formulas";
}
else { // [ctrl-x]
cmd = "cut "+sel+" formulas";
}
editor.EditorScheduleSheetCommands(cmd, true, false); // queue up command to put on SocialCalc clipboard
ta.style.display = "block";
ta.value = cliptext; // must follow "block" setting for Webkit
ta.focus();
ta.select();
window.setTimeout(function() {
var ta = editor.pasteTextarea;
ta.blur();
ta.style.display = "none";
SocialCalc.KeyboardFocus();
}, 200);
return true;
case "[ctrl-v]":
if (editor.noEdit || editor.ECellReadonly()) return true; // not if no edit
ta = editor.pasteTextarea;
ta.value = "";
cell=SocialCalc.GetEditorCellElement(editor, editor.ecell.row, editor.ecell.col);
if (cell) {
position = SocialCalc.GetElementPosition(cell.element);
ta.style.left = (position.left-1)+"px";
ta.style.top = (position.top-1)+"px";
}
ta.style.display = "block";
ta.value = ""; // must follow "block" setting for Webkit
ta.focus();
window.setTimeout(function() {
var ta = editor.pasteTextarea;
var value = ta.value;
ta.blur();
ta.style.display = "none";
var cmd = "";
var clipstr = SocialCalc.ConvertSaveToOtherFormat(SocialCalc.Clipboard.clipboard, "tab");
value = value.replace(/\r\n/g, "\n");
// pastes SocialCalc clipboard if did a Ctrl-C and contents still the same
// Webkit adds an extra blank line, so need to allow for that
if (value != clipstr && (value.length-clipstr.length!=1 || value.substring(0,value.length-1)!=clipstr)) {
cmd = "loadclipboard "+
SocialCalc.encodeForSave(SocialCalc.ConvertOtherFormatToSave(value, "tab")) + "\n";
}
var cr;
if (editor.range.hasrange) {
var clipsheet = new SocialCalc.Sheet();
clipsheet.ParseSheetSave(SocialCalc.Clipboard.clipboard);
var matches = clipsheet.copiedfrom.match(/(.+):(.+)/);
if (matches !== null && matches[1] === matches[2]) {
// copy one cell to selected range
cr = SocialCalc.crToCoord(editor.range.left, editor.range.top) +
':' + SocialCalc.crToCoord(editor.range.right, editor.range.bottom);
} else {
cr = SocialCalc.crToCoord(editor.range.left, editor.range.top);
}
}
else {
cr = editor.ecell.coord;
}
cmd += "paste "+cr+" formulas";
editor.EditorScheduleSheetCommands(cmd, true, false);
SocialCalc.KeyboardFocus();
}, 200);
return true;
case "[ctrl-z]":
editor.EditorScheduleSheetCommands("undo", true, false);
return false;
case "[ctrl-s]": // !!!! temporary hack
if (!SocialCalc.Constants.AllowCtrlS) break;
window.setTimeout(
function() {
var sheet = editor.context.sheetobj;
var cell = sheet.GetAssuredCell(editor.ecell.coord);
var ntvf = cell.nontextvalueformat ? sheet.valueformats[cell.nontextvalueformat-0] || "" : "";
var newntvf = window.prompt("Advanced Feature:\n\nCustom Numeric Format or Command", ntvf);
if (newntvf != null) { // not cancelled
if (newntvf.match(/^cmd:/)) {
cmd = newntvf.substring(4); // execute as command
}
else if (newntvf.match(/^edit:/)) {
cmd = newntvf.substring(5); // execute as command
if (SocialCalc.CtrlSEditor) {
SocialCalc.CtrlSEditor(cmd);
}
return;
}
else {
if (editor.range.hasrange) {
sel = SocialCalc.crToCoord(editor.range.left, editor.range.top)+
":"+SocialCalc.crToCoord(editor.range.right, editor.range.bottom);
}
else {
sel = editor.ecell.coord;
}
cmd = "set "+sel+" nontextvalueformat "+newntvf;
}
editor.EditorScheduleSheetCommands(cmd, true, false);
}
},
200);
return false;
default:
break;
}
return true;
};
// Set sheet's status callback:
context.sheetobj.statuscallback = SocialCalc.EditorSheetStatusCallback;
context.sheetobj.statuscallbackparams = this; // this object: the table editor object
// StatusCallback: all values are called at appropriate times, add with unique name, delete when done
//
// Each value must be an object in the form of:
//
// func: function(editor, status, arg, params) {...},
// params: params value to call func with
//
// The values for status and arg are:
//
// all the SocialCalc RecalcSheet statuscallbacks, including:
//
// calccheckdone, calclist length
// calcorder, {coord: coord, total: celllist length, count: count}
// calcstep, {coord: coord, total: calclist length, count: count}
// calcfinished, time in milliseconds
//
// the command callbacks, like cmdstart and cmdend
// cmdendnorender
//
// calcstart, null
// moveecell, new ecell coord
// rangechange, "coord:coord" or "coord" or ""
// specialkey, keyname ("[esc]")
//
this.StatusCallback = {};
this.MoveECellCallback = {}; // all values are called with editor as arg; add with unique name, delete when done
this.RangeChangeCallback = {}; // all values are called with editor as arg; add with unique name, delete when done
this.SettingsCallbacks = {}; // See SocialCalc.SaveEditorSettings
// Set initial cursor
this.ecell = {coord: "A1", row: 1, col: 1};
context.highlights[this.ecell.coord] = "cursor";
// Initialize range data
// Range has at least hasrange (true/false).
// It may also have: anchorcoord, anchorrow, anchorcol, top, bottom, left, and right.
this.range = {hasrange: false};
// Initialize range2 data (used to show selections, such as for move)
// Range2 has at least hasrange (true/false).
// It may also have: top, bottom, left, and right.
this.range2 = {hasrange: false};
}
// Methods:
SocialCalc.TableEditor.prototype.CreateTableEditor = function(width, height) {return SocialCalc.CreateTableEditor(this, width, height);};
SocialCalc.TableEditor.prototype.ResizeTableEditor = function(width, height) {return SocialCalc.ResizeTableEditor(this, width, height);};
SocialCalc.TableEditor.prototype.SaveEditorSettings = function() {return SocialCalc.SaveEditorSettings(this);};
SocialCalc.TableEditor.prototype.LoadEditorSettings = function(str, flags) {return SocialCalc.LoadEditorSettings(this, str, flags);};
SocialCalc.TableEditor.prototype.EditorRenderSheet = function() {SocialCalc.EditorRenderSheet(this);};
SocialCalc.TableEditor.prototype.EditorScheduleSheetCommands = function(cmdstr, saveundo, ignorebusy) {SocialCalc.EditorScheduleSheetCommands(this, cmdstr, saveundo, ignorebusy);};
SocialCalc.TableEditor.prototype.ScheduleSheetCommands = function(cmdstr, saveundo) {
this.context.sheetobj.ScheduleSheetCommands(cmdstr, saveundo);
};
SocialCalc.TableEditor.prototype.SheetUndo = function() {
this.context.sheetobj.SheetUndo();
};
SocialCalc.TableEditor.prototype.SheetRedo = function() {
this.context.sheetobj.SheetRedo();
};
SocialCalc.TableEditor.prototype.EditorStepSet = function(status, arg) {SocialCalc.EditorStepSet(this, status, arg);};
SocialCalc.TableEditor.prototype.GetStatuslineString = function(status, arg, params) {return SocialCalc.EditorGetStatuslineString(this, status, arg, params);};
SocialCalc.TableEditor.prototype.EditorMouseRegister = function() {return SocialCalc.EditorMouseRegister(this);};
SocialCalc.TableEditor.prototype.EditorMouseUnregister = function() {return SocialCalc.EditorMouseUnregister(this);};
SocialCalc.TableEditor.prototype.EditorMouseRange = function(coord) {return SocialCalc.EditorMouseRange(this, coord);};
SocialCalc.TableEditor.prototype.EditorProcessKey = function(ch, e) {return SocialCalc.EditorProcessKey(this, ch, e);};
SocialCalc.TableEditor.prototype.EditorAddToInput = function(str, prefix) {return SocialCalc.EditorAddToInput(this, str, prefix);};
SocialCalc.TableEditor.prototype.DisplayCellContents = function() {return SocialCalc.EditorDisplayCellContents(this);};
SocialCalc.TableEditor.prototype.EditorSaveEdit = function(text) {return SocialCalc.EditorSaveEdit(this, text);};
SocialCalc.TableEditor.prototype.EditorApplySetCommandsToRange = function(cmdline, type) {return SocialCalc.EditorApplySetCommandsToRange(this, cmdline, type);};
SocialCalc.TableEditor.prototype.MoveECellWithKey = function(ch) {return SocialCalc.MoveECellWithKey(this, ch);};
SocialCalc.TableEditor.prototype.MoveECell = function(newcell) {return SocialCalc.MoveECell(this, newcell);};
SocialCalc.TableEditor.prototype.ReplaceCell = function(cell, row, col) {SocialCalc.ReplaceCell(this, cell, row, col);};
SocialCalc.TableEditor.prototype.UpdateCellCSS = function(cell, row, col) {SocialCalc.UpdateCellCSS(this, cell, row, col);};
SocialCalc.TableEditor.prototype.SetECellHeaders = function(selected) {SocialCalc.SetECellHeaders(this, selected);};
SocialCalc.TableEditor.prototype.EnsureECellVisible = function() {SocialCalc.EnsureECellVisible(this);};
SocialCalc.TableEditor.prototype.ECellReadonly = function(coord) {return SocialCalc.ECellReadonly(this, coord);};
SocialCalc.TableEditor.prototype.RangeAnchor = function(coord) {SocialCalc.RangeAnchor(this, coord);};
SocialCalc.TableEditor.prototype.RangeExtend = function(coord) {SocialCalc.RangeExtend(this, coord);};
SocialCalc.TableEditor.prototype.RangeRemove = function() {SocialCalc.RangeRemove(this);};
SocialCalc.TableEditor.prototype.Range2Remove = function() {SocialCalc.Range2Remove(this);};
SocialCalc.TableEditor.prototype.FitToEditTable = function() {SocialCalc.FitToEditTable(this);};
SocialCalc.TableEditor.prototype.CalculateEditorPositions = function() {SocialCalc.CalculateEditorPositions(this);};
SocialCalc.TableEditor.prototype.ScheduleRender = function() {SocialCalc.ScheduleRender(this);};
SocialCalc.TableEditor.prototype.DoRenderStep = function() {SocialCalc.DoRenderStep(this);};
SocialCalc.TableEditor.prototype.SchedulePositionCalculations = function() {SocialCalc.SchedulePositionCalculations(this);};
SocialCalc.TableEditor.prototype.DoPositionCalculations = function() {SocialCalc.DoPositionCalculations(this);};
SocialCalc.TableEditor.prototype.CalculateRowPositions = function(panenum, positions, sizes) {return SocialCalc.CalculateRowPositions(this, panenum, positions, sizes);};
SocialCalc.TableEditor.prototype.CalculateColPositions = function(panenum, positions, sizes) {return SocialCalc.CalculateColPositions(this, panenum, positions, sizes);};
SocialCalc.TableEditor.prototype.ScrollRelative = function(vertical, amount) {SocialCalc.ScrollRelative(this, vertical, amount);};
SocialCalc.TableEditor.prototype.ScrollRelativeBoth = function(vamount, hamount) {SocialCalc.ScrollRelativeBoth(this, vamount, hamount);};
SocialCalc.TableEditor.prototype.PageRelative = function(vertical, direction) {SocialCalc.PageRelative(this, vertical, direction);};
SocialCalc.TableEditor.prototype.LimitLastPanes = function() {SocialCalc.LimitLastPanes(this);};
SocialCalc.TableEditor.prototype.ScrollTableUpOneRow = function() {return SocialCalc.ScrollTableUpOneRow(this);};
SocialCalc.TableEditor.prototype.ScrollTableDownOneRow = function() {return SocialCalc.ScrollTableDownOneRow(this);};
SocialCalc.TableEditor.prototype.ScrollTableLeftOneCol = function() {return SocialCalc.ScrollTableLeftOneCol(this);};
SocialCalc.TableEditor.prototype.ScrollTableRightOneCol = function() {return SocialCalc.ScrollTableRightOneCol(this);};
// Functions:
SocialCalc.CreateTableEditor = function(editor, width, height) {
var scc = SocialCalc.Constants;
var AssignID = SocialCalc.AssignID;
editor.toplevel = document.createElement("div");
editor.toplevel.style.position = "relative";
AssignID(editor, editor.toplevel, "toplevel");
editor.width = width;
editor.height = height;
editor.griddiv = document.createElement("div");
editor.tablewidth = Math.max(0, width - scc.defaultTableControlThickness);
editor.tableheight = Math.max(0, height - scc.defaultTableControlThickness);
editor.griddiv.style.width = editor.tablewidth+"px";
editor.griddiv.style.height = editor.tableheight+"px";
editor.griddiv.style.overflow = "hidden";
editor.griddiv.style.cursor = "default";
if (scc.cteGriddivClass) editor.griddiv.className = scc.cteGriddivClass;
AssignID(editor, editor.griddiv, "griddiv");
editor.FitToEditTable();
editor.EditorRenderSheet();
editor.griddiv.appendChild(editor.fullgrid);
editor.verticaltablecontrol = new SocialCalc.TableControl(editor, true, editor.tableheight);
editor.verticaltablecontrol.CreateTableControl();
AssignID(editor, editor.verticaltablecontrol.main, "tablecontrolv");
editor.horizontaltablecontrol = new SocialCalc.TableControl(editor, false, editor.tablewidth);
editor.horizontaltablecontrol.CreateTableControl();
AssignID(editor, editor.horizontaltablecontrol.main, "tablecontrolh");
var table, tbody, tr, td, img, anchor, ta;
table = document.createElement("table");
editor.layouttable = table;
table.cellSpacing = 0;
table.cellPadding = 0;
AssignID(editor, table, "layouttable");
tbody = document.createElement("tbody");
table.appendChild(tbody);
tr = document.createElement("tr");
tbody.appendChild(tr);
td = document.createElement("td");
td.appendChild(editor.griddiv);
tr.appendChild(td);
td = document.createElement("td");
td.appendChild(editor.verticaltablecontrol.main);
tr.appendChild(td);
tr = document.createElement("tr");
tbody.appendChild(tr);
td = document.createElement("td");
td.appendChild(editor.horizontaltablecontrol.main);
tr.appendChild(td);
td = document.createElement("td"); // logo display: Required by CPAL License for this code!
td.style.background="url("+editor.imageprefix+"logo.gif) no-repeat center center";
td.innerHTML = "<div style='cursor:pointer;font-size:1px;'><img src='"+editor.imageprefix+"1x1.gif' border='0' width='18' height='18'></div>";
tr.appendChild(td);
editor.logo = td;
AssignID(editor, editor.logo, "logo");
SocialCalc.TooltipRegister(td.firstChild.firstChild, "SocialCalc", null, editor.toplevel);
editor.toplevel.appendChild(editor.layouttable);
if (!editor.noEdit) {
editor.inputEcho = new SocialCalc.InputEcho(editor);
AssignID(editor, editor.inputEcho.main, "inputecho");
}
editor.cellhandles = new SocialCalc.CellHandles(editor);
ta = document.createElement("textarea"); // used for ctrl-c/ctrl-v where an invisible text area is needed
SocialCalc.setStyles(ta, "display:none;position:absolute;height:1px;width:1px;opacity:0;filter:alpha(opacity=0);");
ta.value = "";
editor.pasteTextarea = ta;
AssignID(editor, editor.pasteTextarea, "pastetextarea");
if (navigator.userAgent.match(/Safari\//) &&!navigator.userAgent.match(/Chrome\//)) { // special code for Safari 5 change
window.removeEventListener('beforepaste', SocialCalc.SafariPasteFunction, false);
window.addEventListener('beforepaste', SocialCalc.SafariPasteFunction, false);
window.removeEventListener('beforecopy', SocialCalc.SafariPasteFunction, false);
window.addEventListener('beforecopy', SocialCalc.SafariPasteFunction, false);
window.removeEventListener('beforecut', SocialCalc.SafariPasteFunction, false);
window.addEventListener('beforecut', SocialCalc.SafariPasteFunction, false);
}
editor.toplevel.appendChild(editor.pasteTextarea);
SocialCalc.MouseWheelRegister(editor.toplevel, {WheelMove: SocialCalc.EditorProcessMouseWheel, editor: editor});
SocialCalc.KeyboardSetFocus(editor);
// do status reporting things
SocialCalc.EditorSheetStatusCallback(null, "startup", null, editor);
// done
return editor.toplevel;
}
// Special code needed for change that occurred with Safari 5 that made paste not work for some reason
SocialCalc.SafariPasteFunction = function(e) {
e.preventDefault();
}
//
// SocialCalc.ResizeTableEditor(editor, width, height)
//
// Move things around as appropriate and resize
//
SocialCalc.ResizeTableEditor = function(editor, width, height) {
var scc = SocialCalc.Constants;
editor.width = width;
editor.height = height;
editor.toplevel.style.width = width+"px";
editor.toplevel.style.height = height+"px";
editor.tablewidth = Math.max(0, width - scc.defaultTableControlThickness);
editor.tableheight = Math.max(0, height - scc.defaultTableControlThickness);
editor.griddiv.style.width=editor.tablewidth+"px";
editor.griddiv.style.height=editor.tableheight+"px";
editor.verticaltablecontrol.main.style.height = editor.tableheight + "px";
editor.horizontaltablecontrol.main.style.width = editor.tablewidth + "px";
editor.FitToEditTable();
editor.ScheduleRender();
return;
}
//
// str = SaveEditorSettings(editor)
//
// Returns a string representation of the pane settings, etc.
//
// The format is:
//
// version:1.0
// rowpane:panenumber:firstnum:lastnum
// colpane:panenumber:firstnum:lastnum
// ecell:coord -- if set
// range:anchorcoord:top:bottom:left:right -- if set
//
// You can add additional values to be saved by using editor.SettingsCallbacks:
//
// editor.SettingsCallbacks["item-name"] = {save: savefunction, load: loadfunction}
//
// where savefunction(editor, "item-name") returns a string with the new lines to be added to the saved settings
// which include the trailing newlines, and loadfunction(editor, "item-name", line, flags) is given the line to process
// without the trailing newlines.
//
SocialCalc.SaveEditorSettings = function(editor) {
var i, setting;
var context = editor.context;
var range = editor.range;
var result = "";
result += "version:1.0\n";
for (i=0; i<context.rowpanes.length; i++) {
result += "rowpane:"+i+":"+context.rowpanes[i].first+":"+context.rowpanes[i].last+"\n";
}
for (i=0; i<context.colpanes.length; i++) {
result += "colpane:"+i+":"+context.colpanes[i].first+":"+context.colpanes[i].last+"\n";
}
if (editor.ecell) {
result += "ecell:"+editor.ecell.coord+"\n";
}
if (range.hasrange) {
result += "range:"+range.anchorcoord+":"+range.top+":"+range.bottom+":"+range.left+":"+range.right+"\n";
}
for (setting in editor.SettingsCallbacks) {
result += editor.SettingsCallbacks[setting].save(editor, setting);
}
return result;
}
//
// LoadEditorSettings(editor, str, flags)
//
// Sets the editor settings based on str. See SocialCalc.SaveEditorSettings for more details.
// Unrecognized lines are ignored.
//
SocialCalc.LoadEditorSettings = function(editor, str, flags) {
var lines=str.split(/\r\n|\n/);
var parts=[];
var line, i, cr, row, col, coord, setting;
var context = editor.context;
var highlights, range;
context.rowpanes = [{first: 1, last: 1}]; // reset to start
context.colpanes = [{first: 1, last: 1}];
editor.ecell = null;
editor.range = {hasrange: false};
editor.range2 = {hasrange: false};
range = editor.range;
context.highlights = {};
highlights = context.highlights;
for (i=0; i<lines.length; i++) {
line=lines[i];
parts = line.split(":");
setting = parts[0];
switch (setting) {
case "version":
break;
case "rowpane":
context.rowpanes[parts[1]-0] = {first: parts[2]-0, last: parts[3]-0};
break;
case "colpane":
context.colpanes[parts[1]-0] = {first: parts[2]-0, last: parts[3]-0};
break;
case "ecell":
editor.ecell = SocialCalc.coordToCr(parts[1]);
editor.ecell.coord = parts[1];
highlights[parts[1]] = "cursor";
break;
case "range":
range.hasrange = true;
range.anchorcoord = parts[1];
cr = SocialCalc.coordToCr(range.anchorcoord);
range.anchorrow = cr.row;
range.anchorcol = cr.col;
range.top = parts[2]-0;
range.bottom = parts[3]-0;
range.left = parts[4]-0;
range.right = parts[5]-0;
for (row=range.top; row<=range.bottom; row++) {
for (col=range.left; col<=range.right; col++) {
coord = SocialCalc.crToCoord(col, row);
if (highlights[coord]!="cursor") {
highlights[coord] = "range";
}
}
}
break;
default:
if (editor.SettingsCallbacks[setting]) {
editor.SettingsCallbacks[setting].load(editor, setting, line, flags);
}
break;
}
}
return;
}
//
// EditorRenderSheet(editor)
//
// Renders the sheet and updates editor.fullgrid.
// Sets event handlers.
//
SocialCalc.EditorRenderSheet = function(editor) {
editor.EditorMouseUnregister();
editor.fullgrid = editor.context.RenderSheet(editor.fullgrid);
if (editor.ecell) editor.SetECellHeaders("selected");
SocialCalc.AssignID(editor, editor.fullgrid, "fullgrid"); // give it an id
editor.EditorMouseRegister();
}
//
// EditorScheduleSheetCommands(editor, cmdstr, saveundo, ignorebusy)
//
SocialCalc.EditorScheduleSheetCommands = function(editor, cmdstr, saveundo, ignorebusy) {
if (editor.state!="start" && !ignorebusy) { // ignore commands if editing a cell
return;
}
if (editor.busy && !ignorebusy) { // hold off on commands if doing one
editor.deferredCommands.push({cmdstr: cmdstr, saveundo: saveundo});
return;
}
switch (cmdstr) {
case "recalc":
case "redisplay":
editor.context.sheetobj.ScheduleSheetCommands(cmdstr, false);
break;
case "undo":
editor.SheetUndo();
break;
case "redo":
editor.SheetRedo();
break;
default:
editor.context.sheetobj.ScheduleSheetCommands(cmdstr, saveundo);
break;
}
}
//
// EditorSheetStatusCallback(recalcdata, status, arg, editor)
//
// Called during recalc, executing commands, etc.
//
SocialCalc.EditorSheetStatusCallback = function(recalcdata, status, arg, editor) {
var f, cell, dcmd;
var sheetobj = editor.context.sheetobj;
var signalstatus = function(s) {
for (f in editor.StatusCallback) {
if (editor.StatusCallback[f].func) {
editor.StatusCallback[f].func(editor, s, arg, editor.StatusCallback[f].params);
}
}
}
switch (status) {
case "startup":
break;
case "cmdstart":
editor.busy = true;
sheetobj.celldisplayneeded = "";
break;
case "cmdextension":
break;
case "cmdend":
signalstatus(status);
if (sheetobj.changedrendervalues) {
editor.context.PrecomputeSheetFontsAndLayouts();
editor.context.CalculateCellSkipData();
sheetobj.changedrendervalues = false;
}
if (sheetobj.celldisplayneeded && !sheetobj.renderneeded) {
cr = SocialCalc.coordToCr(sheetobj.celldisplayneeded);
cell = SocialCalc.GetEditorCellElement(editor, cr.row, cr.col);
editor.ReplaceCell(cell, cr.row, cr.col);
}
if (editor.deferredCommands.length) {
dcmd = editor.deferredCommands.shift();
editor.EditorScheduleSheetCommands(dcmd.cmdstr, dcmd.saveundo, true);
return;
}
if (sheetobj.attribs.needsrecalc &&
(sheetobj.attribs.recalc!="off" || sheetobj.recalconce)
&& editor.recalcFunction) {
editor.FitToEditTable();
sheetobj.renderneeded = false; // recalc will force a render
if (sheetobj.recalconce) delete sheetobj.recalconce; // only do once
editor.recalcFunction(editor);
}
else {
if (sheetobj.renderneeded) {
editor.FitToEditTable();
sheetobj.renderneeded = false;
editor.ScheduleRender();
}
else {
editor.SchedulePositionCalculations(); // just in case command changed positions
// editor.busy = false;
// signalstatus("cmdendnorender");
}
}
// Handle hidden column.
if (sheetobj.hiddencolrow == "col") {
var col = editor.ecell.col;
while (sheetobj.colattribs.hide[SocialCalc.rcColname(col)] == "yes") {
col++;
}
var coord = SocialCalc.crToCoord(col, editor.ecell.row);
editor.MoveECell(coord);
sheetobj.hiddencolrow = "";
}
// Handle hidden row.
if (sheetobj.hiddencolrow == "row") {
var row = editor.ecell.row;
while (sheetobj.rowattribs.hide[row] == "yes") {
row++;
}
var coord = SocialCalc.crToCoord(editor.ecell.col, row);
editor.MoveECell(coord);
sheetobj.hiddencolrow = "";
}
return;
case "calcstart":
editor.busy = true;
break;
case "calccheckdone":
case "calcorder":
case "calcstep":
case "calcloading":
case "calcserverfunc":
break;
case "calcfinished":
signalstatus(status);
editor.ScheduleRender();
return;
case "schedrender":
editor.busy = true; // in case got here without cmd or recalc
break;
case "renderdone":
break;
case "schedposcalc":
editor.busy = true; // in case got here without cmd or recalc
break;
case "doneposcalc":
if (editor.deferredCommands.length) {
signalstatus(status);
dcmd = editor.deferredCommands.shift();
editor.EditorScheduleSheetCommands(dcmd.cmdstr, dcmd.saveundo, true);
}
else {
editor.busy = false;
signalstatus(status);
if (editor.state=="start") editor.DisplayCellContents(); // make sure up to date
}
return;
default:
addmsg("Unknown status: "+status);
break;
}
signalstatus(status);
return;
}
//
// str = SocialCalc.EditorGetStatuslineString(editor, status, arg, params)
//
// Assumes params is an object where it can use "calculating" and "command"
// to keep track of state.
// Returns string for status line.
//
SocialCalc.EditorGetStatuslineString = function(editor, status, arg, params) {
var scc = SocialCalc.Constants;
var sstr, progress, coord, circ, r, c, cell, sum, ele;
progress = "";
switch (status) {
case "moveecell":
case "rangechange":
case "startup":
break;
case "cmdstart":
params.command = true;
document.body.style.cursor = "progress";
editor.griddiv.style.cursor = "progress";
progress = scc.s_statusline_executing;
break;
case "cmdextension":
progress = "Command Extension: "+arg;
break;
case "cmdend":
params.command = false;
break;
case "schedrender":
progress = scc.s_statusline_displaying;
break;
case "renderdone":
progress = " ";
break;
case "schedposcalc":
progress = scc.s_statusline_displaying;
break;
case "cmdendnorender":
case "doneposcalc":
document.body.style.cursor = "default";
editor.griddiv.style.cursor = "default";
break;
case "calcorder":
progress = scc.s_statusline_ordering+Math.floor(100*arg.count/(arg.total||1))+"%";
break;
case "calcstep":
progress = scc.s_statusline_calculating+Math.floor(100*arg.count/(arg.total||1))+"%";
break;
case "calcloading":
progress = scc.s_statusline_calculatingls+": "+arg.sheetname;
break;
case "calcserverfunc":
progress = scc.s_statusline_calculating+Math.floor(100*arg.count/(arg.total||1))+"%, "+scc.s_statusline_doingserverfunc+arg.funcname+scc.s_statusline_incell+arg.coord;
break;
case "calcstart":
params.calculating = true;
document.body.style.cursor = "progress";
editor.griddiv.style.cursor = "progress"; // griddiv has an explicit cursor style
progress = scc.s_statusline_calcstart;
break;
case "calccheckdone":
break;
case "calcfinished":
params.calculating = false;
break;
default:
progress = status;
break;
}
if (!progress && params.calculating) {
progress = scc.s_statusline_calculating;
}
// if there is a range, calculate sum (not during busy times)
if (!params.calculating && !params.command && !progress && editor.range.hasrange
&& (editor.range.left!=editor.range.right || editor.range.top!=editor.range.bottom)) {
sum = 0;
for (r=editor.range.top; r <= editor.range.bottom; r++) {
for (c=editor.range.left; c <= editor.range.right; c++) {
cell = editor.context.sheetobj.cells[SocialCalc.crToCoord(c, r)];
if (!cell) continue;
if (cell.valuetype && cell.valuetype.charAt(0)=="n") {
sum += cell.datavalue-0;
}
}
}
sum = SocialCalc.FormatNumber.formatNumberWithFormat(sum, "[,]General", "");
coord = SocialCalc.crToCoord(editor.range.left, editor.range.top) + ":" +
SocialCalc.crToCoord(editor.range.right, editor.range.bottom);
progress = coord + " (" + (editor.range.right-editor.range.left+1) + "x" + (editor.range.bottom-editor.range.top+1) +
") "+scc.s_statusline_sum+"=" + sum + " " + progress;
}
sstr = (editor.ecell || {}).coord+" "+progress;
if (!params.calculating && editor.context.sheetobj.attribs.needsrecalc=="yes") {
sstr += ' '+scc.s_statusline_recalcneeded;
}
circ = editor.context.sheetobj.attribs.circularreferencecell;
if (circ) {
circ = circ.replace(/\|/, " referenced by ");
sstr += ' '+scc.s_statusline_circref + circ + '</span>';
}
return sstr;
}
//
// Mouse stuff
//
SocialCalc.EditorMouseInfo = {
// The registeredElements array is used to identify editor grid in which the mouse is doing things.
// One item for each active editor, each an object with:
// .element, .editor
registeredElements: [],
editor: null, // editor being processed (between mousedown and mouseup)
element: null, // element being processed
ignore: false, // if true, mousedowns are ignored
mousedowncoord: "", // coord where mouse went down for drag range
mouselastcoord: "", // coord where mouse last was during drag
mouseresizecol: "", // col being resized
mouseresizeclientx: null, // where resize started
mouseresizedisplay: null // element tracking new size
}
//
// EditorMouseRegister(editor)
//
SocialCalc.EditorMouseRegister = function(editor) {
var mouseinfo = SocialCalc.EditorMouseInfo;
var element = editor.fullgrid;
var i;
for (i=0; i<mouseinfo.registeredElements.length; i++) {
if (mouseinfo.registeredElements[i].editor == editor) {
if (mouseinfo.registeredElements[i].element == element) {
return; // already set - don't do it again
}
break;
}
}
if (i<mouseinfo.registeredElements.length) {
mouseinfo.registeredElements[i].element = element;
}
else {
mouseinfo.registeredElements.push({element: element, editor: editor});
}
if (element.addEventListener) { // DOM Level 2 -- Firefox, et al
element.addEventListener("mousedown", SocialCalc.ProcessEditorMouseDown, false);
element.addEventListener("dblclick", SocialCalc.ProcessEditorDblClick, false);
}
else if (element.attachEvent) { // IE 5+
element.attachEvent("onmousedown", SocialCalc.ProcessEditorMouseDown);
element.attachEvent("ondblclick", SocialCalc.ProcessEditorDblClick);
}
else { // don't handle this
throw "Browser not supported";
}
mouseinfo.ignore = false; // just in case
return;
}
//
// EditorMouseUnregister(editor)
//
SocialCalc.EditorMouseUnregister = function(editor) {
var mouseinfo = SocialCalc.EditorMouseInfo;
var element = editor.fullgrid;
var i, oldelement;
for (i=0; i<mouseinfo.registeredElements.length; i++) {
if (mouseinfo.registeredElements[i].editor == editor) {
break;
}
}
if (i<mouseinfo.registeredElements.length) {
oldelement = mouseinfo.registeredElements[i].element; // remove old handlers
if (oldelement.removeEventListener) { // DOM Level 2
oldelement.removeEventListener("mousedown", SocialCalc.ProcessEditorMouseDown, false);
oldelement.removeEventListener("dblclick", SocialCalc.ProcessEditorDblClick, false);
}
else if (oldelement.detachEvent) { // IE
oldelement.detachEvent("onmousedown", SocialCalc.ProcessEditorMouseDown);
oldelement.detachEvent("ondblclick", SocialCalc.ProcessEditorDblClick);
}
mouseinfo.registeredElements.splice(i, 1);
}
return;
}
SocialCalc.ProcessEditorMouseDown = function(e) {
var editor, result, coord, textarea, wval, range;
var event = e || window.event;
var mouseinfo = SocialCalc.EditorMouseInfo;
var ele = event.target || event.srcElement; // source object is often within what we want
var mobj;
if (mouseinfo.ignore) return; // ignore this
for (mobj=null; !mobj && ele; ele=ele.parentNode) { // go up tree looking for one of our elements
mobj = SocialCalc.LookupElement(ele, mouseinfo.registeredElements);
}
if (!mobj) {
mouseinfo.editor = null;
return; // not one of our elements
}
editor = mobj.editor;
mouseinfo.element = ele;
range = editor.range;
var pos = SocialCalc.GetElementPositionWithScroll(editor.toplevel);
var clientX = event.clientX - pos.left;
var clientY = event.clientY - pos.top;
result = SocialCalc.GridMousePosition(editor, clientX, clientY);
if (!result) return; // not on a cell or col header
mouseinfo.editor = editor; // remember for later
if (result.rowheader) {
if (result.rowselect) {
SocialCalc.ProcessEditorRowselectMouseDown(editor, result);
} else {
SocialCalc.ProcessEditorRowsizeMouseDown(e, ele, result);
}
return;
}
if (result.colheader) {
if (result.colselect) {
SocialCalc.ProcessEditorColselectMouseDown(editor, result);
} else {
SocialCalc.ProcessEditorColsizeMouseDown(e, ele, result);
}
return;
}
if (!result.coord) return; // not us
if (!range.hasrange) {
if (e.shiftKey)
editor.RangeAnchor();
}
coord = editor.MoveECell(result.coord);
if (range.hasrange) {
if (e.shiftKey)
editor.RangeExtend();
else
editor.RangeRemove();
}
mouseinfo.mousedowncoord = coord; // remember if starting drag range select
mouseinfo.mouselastcoord = coord;
editor.EditorMouseRange(coord);
SocialCalc.KeyboardSetFocus(editor);
if (editor.state!="start" && editor.inputBox) editor.inputBox.element.focus();
// Event code from JavaScript, Flanagan, 5th Edition, pg. 422
if (document.addEventListener) { // DOM Level 2 -- Firefox, et al
document.addEventListener("mousemove", SocialCalc.ProcessEditorMouseMove, true); // capture everywhere
document.addEventListener("mouseup", SocialCalc.ProcessEditorMouseUp, true); // capture everywhere
}
else if (ele.attachEvent) { // IE 5+
ele.setCapture();
ele.attachEvent("onmousemove", SocialCalc.ProcessEditorMouseMove);
ele.attachEvent("onmouseup", SocialCalc.ProcessEditorMouseUp);
ele.attachEvent("onlosecapture", SocialCalc.ProcessEditorMouseUp);
}
if (event.stopPropagation) event.stopPropagation(); // DOM Level 2
else event.cancelBubble = true; // IE 5+
if (event.preventDefault) event.preventDefault(); // DOM Level 2
else event.returnValue = false; // IE 5+
return;
}
SocialCalc.EditorMouseRange = function(editor, coord) {
var inputtext, wval;
var range = editor.range;
switch (editor.state) { // editing a cell - shouldn't get here if no inputBox
case "input":
inputtext = editor.inputBox.GetText();
wval = editor.workingvalues;
if (("(+-*/,:!&<>=^".indexOf(inputtext.slice(-1))>=0 && inputtext.slice(0,1)=="=") ||
(inputtext == "=")) {
wval.partialexpr = inputtext;
}
if (wval.partialexpr) { // if in pointing operation
if (coord) {
if (range.hasrange) {
editor.inputBox.SetText(wval.partialexpr + SocialCalc.crToCoord(range.left, range.top) + ":" +
SocialCalc.crToCoord(range.right, range.bottom));
}
else {
editor.inputBox.SetText(wval.partialexpr + coord);
}
}
}
else { // not in point -- done editing
editor.inputBox.Blur();
editor.inputBox.ShowInputBox(false);
editor.state = "start";
editor.cellhandles.ShowCellHandles(true);
editor.EditorSaveEdit();
editor.inputBox.DisplayCellContents(null);
}
break;
case "inputboxdirect":
editor.inputBox.Blur();
editor.inputBox.ShowInputBox(false);
editor.state = "start";
editor.cellhandles.ShowCellHandles(true);
editor.EditorSaveEdit();
editor.inputBox.DisplayCellContents(null);
break;
}
}
SocialCalc.ProcessEditorMouseMove = function(e) {
var editor, element, result, coord, now, textarea, sheetobj, cellobj, wval;
var event = e || window.event;
var mouseinfo = SocialCalc.EditorMouseInfo;
editor = mouseinfo.editor;
if (!editor) return; // not us, ignore
if (mouseinfo.ignore) return; // ignore this
element = mouseinfo.element;
var pos = SocialCalc.GetElementPositionWithScroll(editor.toplevel);
var clientX = event.clientX - pos.left;
var clientY = event.clientY - pos.top;
result = SocialCalc.GridMousePosition(editor, clientX, clientY); // get cell with move
if (!result) return;
if (result && !result.coord) {
SocialCalc.SetDragAutoRepeat(editor, result);
return;
}
SocialCalc.SetDragAutoRepeat(editor, null); // stop repeating if it was
if (!result.coord) return;
if (result.coord!=mouseinfo.mouselastcoord) {
if (!e.shiftKey && !editor.range.hasrange) {
editor.RangeAnchor(mouseinfo.mousedowncoord);
}
editor.MoveECell(result.coord);
editor.RangeExtend();
}
mouseinfo.mouselastcoord = result.coord;
editor.EditorMouseRange(result.coord);
if (event.stopPropagation) event.stopPropagation(); // DOM Level 2
else event.cancelBubble = true; // IE 5+
if (event.preventDefault) event.preventDefault(); // DOM Level 2
else event.returnValue = false; // IE 5+
return;
}
SocialCalc.ProcessEditorMouseUp = function(e) {
var editor, element, result, coord, now, textarea, sheetobj, cellobj, wval;
var event = e || window.event;
var mouseinfo = SocialCalc.EditorMouseInfo;
editor = mouseinfo.editor;
if (!editor) return; // not us, ignore
if (mouseinfo.ignore) return; // ignore this
element = mouseinfo.element;
var pos = SocialCalc.GetElementPositionWithScroll(editor.toplevel);
var clientX = event.clientX - pos.left;
var clientY = event.clientY - pos.top;
result = SocialCalc.GridMousePosition(editor, clientX, clientY); // get cell with up
SocialCalc.SetDragAutoRepeat(editor, null); // stop repeating if it was
if (!result) return;
if (!result.coord) result.coord = editor.ecell.coord;
if (editor.range.hasrange) {