Programmer Documentation for the SocialCalc Engine Javascript Revision 6, 17 July 2007 Originally authored by Dan Bricklin This document explains the Javascript code that is part of the SocialCalc Engine. It starts with an overview of the routines and data structures, and then demonstrates their use with some sample code. Finally it describes some of the major routines in more detail. The SocialCalc Engine Javascript consists of two files in the jslib/SocialCalc directory: Common.js and Edit.js. The Common.js file contains routines that are used for the Edit tab, the Format tab, and the Backup command. The Edit.js file has routines specific to the Edit tab. Overview of Functionality and Variables Javascript is used by SocialCalc for a variety of purposes. It is used to allow user control of the system with the mouse and keyboard. It is used to modify the display of various blocks of text and controls in response to mouse clicks and key presses, such as displaying help text and implementing the "/More" command tree and the ":Range" commands. It is used to scroll the spreadsheet display. It is used to communicate via the XMLHttpRequest object with the server when making value changes to cells ("Ajax style" interaction). It is used to communicate with the server to get lists of other pages and text for the help display. The Javascript code grew as the original wikiCalc code went through successive alpha and then beta versions. The initial design made little use of Javascript, relying on frequent trips to the server for complete screen refreshes. The code was written to run on a wide range of browsers, old and new. As the project progressed, it became apparent that a more interactive system was needed and more and more use of Ajax techniques was made, added on top of the old code. The new functionality was coded directly without use of a separate off-the-shelf library. The code reflects this evolution. The first SocialCalc release (1.1.0) introduced the use of an off-the-shelf library, Mootools, but that was only used to add keyboard support for shift-Tab. In future releases further use of this library is planned. Some Common Variables The Common.js file includes the declarations of the main common variables. These include the major state variables. The jsedit variable is true while editing a cell directly. That is, it is true when the characters being typed are to be reflected directly in the cell as well as in the formula bar text box (that text box is in the "valueedit" input tag). This is a "smart" type of input such that arrow keys or mouse clicks on cells do the "right" thing depending upon whether the last character is an operator (for example if the last character is "+" the pointed to cell's coordinates would be automatically entered into the text being typed in) or not (the entry would be considered complete and the spreadsheet updated). The veedit variable is true while editing directly in the formula bar text box. This state allows normal keyboard control of this editing. For example, the arrow keys move the editing cursor in the text box and editing operations such as Ctrl-C/Ctrl-V/Copy/Paste work, depending upon normal browser operation. The vetedit variable is true while editing in the multi-line input area (the "valueedittext" textarea tag). The cmdedit variable keeps track of which command characters have been typed (e.g., "/P") as well as the accumulating veedit input. The editconfig variable indicates which configuration of hidden and displayed text areas is being displayed. Other variables keep track of the cell being edited, the state of the help display, etc. The editing environment implements a scrolling grid. This is accomplished by using Javascript to remove and add rows to the original rendered spreadsheet table. To recreate rows that have been scrolled off the top, and to provide the extra information needed for editing cell contents and formats, extra information is stored about each cell. This information goes in the "sheet" variables. They are objects, addressed by cell coordinates (e.g., sheetvals["C3"]), containing the cell types, display values, editing values, alignment, etc., for each defined cell. The parse_sheet Format The "sheet" variables are loaded with data created by the server. The function that takes the server-created data and puts it into these variables is usually called parse_sheet. There are parse_sheet functions in both the Edit.js and jslib/App/SocialCalc/Format.js files. (The Format.js version includes extra code to parse additional global information needed on the Format tab.) The parse_sheet function takes as an argument a string in a multi-line, human-readable format. Each line consists of sections separated by ":" characters. The first section is the line type. Normally the line type is the coordinate of the cell for which the line defines values. The sections of this form of line are: coord:type:display-value:edit-value:alignment:colspan:rowspan:skip:csss-val Where: coord is the cell coordinates (e.g., "C3"), display-value is the text to be displayed in the cell rendered as HTML (e.g., "$4,321.00"), edit-value is the value to show on the edit line (e.g., "4321"), alignment is the CSS alignment setting (e.g., "right"), colspan and rowspan are the values for the tag which allow for merged cells (e.g., "1" for both), skip is the coordinate of the cell to move the cursor to if the cursor lands on this cell or "" if the cell is not one hidden by a merged cell, and csss-val is the added explicit CSS style value for the cell (csss, set on the Format Misc tab). Other forms of lines for the parse_sheet function are "error:Error message" (to set the error message display above the formula bar), "footer:time-string" (to update the time display in the page footer during a long editing session), and "needsrecalc" to set the "May need recalculation" indication when recalc is set to manual. The Format.js version of parse_sheet includes additional line forms to initialize lists of available formats, fonts, etc., for use on drop downs on the Format tab. The parse_sheet routine is used in two main situations. It is used when the page is first displayed to initialize the "sheet" variables. This lets the server compress all of the settings into one string, rather than needing to download multiple variable setting statements. You will see in such situations a Javascript statement in the page source starting with "isheet=...", followed by a "parse_sheet(isheet);" statement. The other situation is during the Ajax interaction when new values are set for cells. The browser makes an XMLHttpRequest to the server, specifying the cell coordinates and the new value. The server responds with text in the parse_sheet format. This string is processed with the parse_sheet function, which keeps track of the cells given updated values. Those cells are then updated in the DOM by the update_screen function and reflected on the screen. Sample Code Showing Use of Common.js and Edit.js Here is a Perl program, simpleedit1.pl, that can act as a testbed and as an illustration of the interaction between a server and the SocialCalc Engine Javascript routines. It is meant to be used in response to a CGI request with a web server. It should be installed in a directory with a sub-directory of "SocialCalc" containing Sheet.pm, SheetFunctions.pm, and Strings.pm from the /lib/SocialCalc directory in the distribution, and a sub-directory of jslib with a subdirectory of SocialCalc containing the Common.js and Edit.js files. It assumes the Javascript files are the same as those in the SocialCalc 1.1.0 release. It is assumed that you are familiar with the documentation of the SocialCalc Engine and the simplesheet1.pl program presented there. The simpleedit1.pl program starts out being similar to simplesheet1.pl. When first invoked, it displays a rendered empty spreadsheet and the save format text that can create it. It allows you to edit that saved text and then re-invoke the program to re-render the sheet from the updated saved data. (It does not have the command interface to issue Sheet.pm commands present in the simplesheet1.pl program since that would just make it longer without teaching anything new.) In addition to doing what simplesheet1.pl does, the simpleedit1.pl program implements a simple system that: Displays the HTML and Javascript for a page that allows simple editing of the spreadsheet. Gives access to many of the facilities of SocialCalc's editing so you can move the cursor around, scroll with Page Up and Page Down keys, and view the contents of selected cells in a formula bar. Allows you to type new values into cells. Uses the XMLHttpRequest method for the browser to tell the server of changes to a cell. Receives the server's response and updates the displayed sheet accordingly. Displays the text of the Ajax request and response as a view into the internal operation. Since this is a minimal implementation using the full Javascript, some of the operations you can try to perform when it is running (such as invoking the "/More" commands) will try to access items in the HTML that are not present and will result in Javascript errors. It does, though, perform a substantial amount of the functionality. Simple editing of the Javascript files can remove those references and make this program more robust. The object, though, of this exercise is to work from the standard released code and be able to be enhanced to test out other functionality. Here is the complete source code (with an explanation following it): --- Start source of simpleedit1.pl --- #!/usr/bin/perl use strict; use SocialCalc::Sheet; use SocialCalc::SheetFunctions; use SocialCalc::Strings; use CGI qw(:standard); my %colorlookup; initialize_colorlookup(); fill_in_colors(\$SocialCalc::Sheet::sheetstyleheader); my $q = new CGI; my @lines = split(/\n/,$q->param("savestr")); # sheet data in textarea my %sheetdata; parse_sheet_save(\@lines, \%sheetdata); if ($q->param("edit")) { # when "edit" button is pressed start_edit(\%sheetdata); } elsif ($q->param("ajaxsetcell")) { # Ajax call to change cell value ajax_call(\%sheetdata, $q->param("ajaxsetcell")); } else { # default - on initial use or when "render" button is pressed recalc_sheet(\%sheetdata); my ($stylestr, $outstr) = render_sheet(\%sheetdata, "", "", "s", "a", "inline", "", "", ""); my $savestr = create_sheet_save(\%sheetdata); print <<"EOF"; Content-type: text/html Simple Sheet With Editing

SIMPLE SPREADSHEET SYSTEM WITH EDITING

Rendered Sheet:


$outstr

Saved Data:



EOF } # # start_edit(\%sheetdata) - render initial editing display # sub start_edit { my $sheetdata = shift @_; my $savestr = create_sheet_save($sheetdata); # Load scripts from a file my $inlinescripts .= $WKCStrings{"jsdefinestrings"}; open JSFILE, "jslib/SocialCalc/Common.js"; while (my $line = ) { $inlinescripts .= $line; } close JSFILE; $inlinescripts .= $WKCStrings{"editjsdefinestrings"}; open JSFILE, "jslib/SocialCalc/Edit.js"; while (my $line = ) { $inlinescripts .= $line; } close JSFILE; fill_in_colors(\$inlinescripts); # patch in passing savestr back and forth $inlinescripts =~ # add to ajaxsetcell_request function s/ (function\ ajaxsetcell_request\(.*?)\ makeRequest / $1 reqcontents = reqcontents + "&savestr="+encodeURIComponent(document.f0.savestr.value); document.f0.ajaxreq.value=reqcontents; makeRequest/xs; $inlinescripts =~ # add to parse_sheet function s/ (function\ parse_sheet\(.*?)\ var\ col / $1 else if (cr=="sheettext") { document.f0.savestr.value=decode_field(parts[1]); document.f0.ajaxres.value=str;continue; } var col/xs; my ($sheetstyle, $sheetstr) = render_sheet($sheetdata, 'id="sheet0" class="wkcsheet"', "", "s", "a", "ajax", "A1", q! onclick="rc0('$coord');"!, ""); my %celldata; # get initial cell settings my ($lcol, $lrow) = render_values_only($sheetdata, \%celldata, ""); my $jsdata = qq!var isheet="";\nisheet="!; # cell information foreach my $cr (sort keys %celldata) { # construct output my $str = prepare_celldata(\%celldata, $sheetdata, $cr); $str =~ s/\\/\\\\/g; $str =~ s/"/\\x22/g; $str =~ s/ $jsdata sheetlastcol=$lcol; sheetlastrow=$lrow; parse_sheet(isheet); ecell="A1"; needsrecalc=""; cliprange="A1"; //dummy to avoid msg EOF print <<"EOF"; # output page with edit JS code Content-type: text/html Simple Sheet With Editing

SIMPLE SPREADSHEET SYSTEM WITH EDITING

$inlinescripts

Editing Sheet:


 
Loading...  
  
$sheetstr

Saved Data:


Last Ajax Request:


Last Ajax Response:



EOF } # # prepare_celldata(\%celldata, \%sheetdata, $cr) - render definition # line for $cr in isheet and ajax response data format # sub prepare_celldata { my ($celldata, $sheetdata, $cr) = @_; my $cellspecifics = $celldata->{$cr}; my $displayvalue = encode_for_save($cellspecifics->{display}); $displayvalue = "" if $displayvalue eq " "; # this is the default my $csssvalue = encode_for_save($sheetdata->{cellattribs}->{$cr}->{csss}); my $editvalue; if ($sheetdata->{datatypes}->{$cr} eq 'f' || $sheetdata->{datatypes}->{$cr} eq 'c') { # formula or constant $editvalue = encode_for_save($sheetdata->{formulas}->{$cr}); } else { $editvalue = encode_for_save($sheetdata->{datavalues}->{$cr}); } my $str = "$cr:$cellspecifics->{type}:$displayvalue:$editvalue:" . "$cellspecifics->{align}:$cellspecifics->{colspan}:" . "$cellspecifics->{rowspan}:$cellspecifics->{skip}:$csssvalue"; return $str; } # # ajax_call(\%sheetdata, $command) - respond to ajaxsetcell request # sub ajax_call { my ($sheetdata, $command) = @_; my ($psite, $pname, $coord, $value) = split(/:/, $command, 4); $value = decode_from_ajax($value); my %celldatabefore; render_values_only($sheetdata, \%celldatabefore, ""); # Determine value type and do appropriate command to set it my $type; my $fch = substr($value, 0, 1); if ($fch eq "=" && $value !~ m/\n/) { $type = "formula"; $value = substr($value, 1); } elsif ($fch eq "'") { $type = "text t"; $value = substr($value, 1); } elsif (length $value == 0) { $type = "empty"; } else { my $v1 = determine_value_type($value, \$type); if ($type eq 'n' && $v1 == $value) { # check we don't need "constant" $type = "value n"; } elsif ($type eq 't') { $type = "text t"; } else { # handle all the special types $type = "constant $type $v1"; } } my $cmdline = "set $coord $type $value"; # create command my $ok = execute_sheet_command($sheetdata, $cmdline); # set cell value my $aerrtext = recalc_sheet($sheetdata); my %celldataafter; render_values_only($sheetdata, \%celldataafter, ""); my $response; foreach my $cr (sort keys %celldataafter) { # construct output my $cdbefore = $celldatabefore{$cr}; my $cdafter = $celldataafter{$cr}; next if $cdbefore->{type} eq $cdafter->{type} && $cdbefore->{display} eq $cdafter->{display} && $cdbefore->{align} eq $cdafter->{align} && $cr ne $coord; # only stuff that has changed unknown to client # (but send at least one -- the one with "loading") my $str = prepare_celldata(\%celldataafter, $sheetdata, $cr); $str =~ s/\\r//g; $str =~ s/]]>/\\e/g; $response .= "$str\n"; } my $savestr = create_sheet_save(\%sheetdata); $response .= "sheettext:" . encode_for_ajax($savestr) . "\n"; print <<"EOF"; Content-type: text/xml EOF } # # fill_in_colors(\$str) - Replaces {[colorname]} in $$str with color value # sub fill_in_colors { my $str = shift @_; $$str =~ s/\{\[(\w+?)\]\}/$colorlookup{$1}/ge; } # # initialize_colorlookup() - Fills in %colorlookup # sub initialize_colorlookup { my %colordata = ( # normally this comes from $WKCStrings "colornames" => "error cursor rangebody rangeend gridheaderbackground gridheadertext gridheaderbackgroundselected gridheadertextselected skippedcellbackground draghandleup draghandledown draghandleoff scrolloff scrollon ", "color-error" => "#FF0000", "color-cursor" => "#394F87", "color-rangebody" => "#EEEEEE", "color-rangeend" => "#80A9F3", #99CC99 "color-gridheaderbackground" => "#80A9F3", "color-gridheadertext" => "#FFFFFF", #FFFFFF "color-gridheaderbackgroundselected" => "#394F87", "color-gridheadertextselected" => "#FFFFFF", "color-skippedcellbackground" => "#CCCCCC", "color-draghandleup" => "#CCCCCC", "color-draghandledown" => "#999999", "color-draghandleoff" => "#80A9F3", "color-scrolloff" => "#FFFFFF", "color-scrollon" => "#999999", ); foreach my $c (split(/\s/, $colordata{colornames})) { $colorlookup{$c} = $colordata{"color-$c"}; } } # # decode_from_ajax($string) - Returns a string with # \n, \b, and \c escaped to \n, \, and : # sub decode_from_ajax { my $string = shift @_; $string =~ s/\\n/\n/g; $string =~ s/\\c/:/g; $string =~ s/\\b/\\/g; return $string; } # # encode_for_ajax($string) - Returns a string with # \n, \, :, and ]]> escaped to \n, \b, \c, and \e # sub encode_for_ajax { my $string = shift @_; $string =~ s/\\/\\b/g; $string =~ s/\n/\\n/g; $string =~ s/\r//g; $string =~ s/:/\\c/g; $string =~ s/]]>/\\e/g; return $string; } --- End source of simpleedit1.pl --- When invoked using a browser, this script should display an empty rendered sheet, a text area labeled "Saved Data" displaying the save format data for the empty sheet, and buttons labeled "Render" and "Edit". To load a more interesting spreadsheet, namely our example from the other documentation, copy the following into the Saved Data text area: version:1.3 cell:A1:t:Value\c:f:1 cell:B1:v:9 cell:A2:t:Square root\c:f:1 cell:B2:vtf:n:3:SQRT(B1) col:A:w:120 sheet:r:2:c:2 font:1:normal bold * * If you then press the "Render" button the page should repaint with the appropriate rendered sheet. You can also copy saved data from SocialCalc save files, such as the wkcdata/sites/demosite/demopage1.edit.demoauthor.txt file created when using the demo setup. The text you should copy is from the "version" line in the last MIME component (e.g., "version:1.2") to the end right before the "--wkc-boundary--" line (e.g., "valueformat:2:#,##0.0"). That particular file (demopage1...) is interesting because it has merged cells and a cell with multi-line text as well as many formulas with cascading values after a recalculation. When you press the "Edit" button you will see the screen replaced by the spreadsheet in a grid with 1-2-3/A-B-C row/column labels. Above the grid is a simple formula bar displaying the contents of the cell with the cursor. Below it is the same Saved Data textarea. Below that are two textareas to show text of the last Ajax request to the server and the server's response. You can use the arrow keys or the mouse to move the cursor to other cells. Their values will be shown in the formula bar. If you type a new value into a cell (either text, a number, or a "=formula") it will echo into the cell and on the formula bar. If you type something like "=1+" and then press the arrow key, the cell coordinates will be added to the formula appropriately. You can also click on the formula bar text area and edit there. If you encounter a cell with a multi-line value and click a second time on the cell, or start typing in a cell with the quote (") character, you will be able to use a textarea for editing that replaces the single-line formula bar on the screen. Pressing Enter or clicking the "OK" button sends the new cell value along with the sheet data to the server using an XMLHttpRequest object. The server responds with parse_sheet-format data to update the sheet as well as updated sheet data to be displayed. The text of the request and the response is shown in the textareas below the sheet. Pressing the "Render" button returns to the normal rendered-spreadsheet display. This program implements a "stateless" spreadsheet editing system. The program running on the server does not remember anything about the sheet between invocations. All of the information comes from forms sent with the request and then populated with updated information in the response. It operates differently than the full SocialCalc program in that it does not require user authentication and all of the intermediate editing files. It can be the seed from which you can figure out how to add simple spreadsheeting to other online content creation systems. The fact that you can implement this with just the SocialCalc Engine components shows why the split between Engine and App was done the way that it was. Here is an explanation of the simpleedit1.pl code itself: The first part of the code initializes the %colorlookup hash. This hash is used to help dynamically fill in the color values for the CSS controlling the look of the sheet when editing as well as any colors used by the Javascript code. Those colors are indicated by [{colorname}] in the text. The fill_in_colors routine (part of simpleedit1.pl along with the initialize_colorlookup routine) does the actual replacements. (In the SocialCalc app, these routines are in Sheet.pm.) The program then retrieves the saved sheet data from the HTTP request and parses it, resulting in a %sheetdata structure. Then, depending upon the type of invocation, either the start_edit routine (for presses of the "Edit" button), the ajax_call routine (for XMLHttpRequest posts that have the "ajaxsetcell" parameter), or plain rendering is performed. The plain rendering is similar to simplesheet1.pl, but it includes the extra button and no command line. The start_edit routine puts together the HTML and Javascript needed for editing and then returns that to the browser. It starts by making a copy of the save data for including in the output. It then reads in the contents of Common.js and Edit.js for inclusion. It replaces the color placeholders with the appropriate color values. In order to have the browser include a copy of the saved data as part of the Ajax interaction (not something the normal use of SocialCalc does -- that information is available to the server from files) the standard text of Edit.js is "patched" before downloading. This lets this sample program run from the standard release of the SocialCalc Engine without including a special version of Edit.js in this documentation. There are two places where a bit of code is added to include sheet data as part of both the request and response and to display the request and response strings. The first place is in the ajaxsetcell_request function where a "savestr" parameter is added to the request and the request textarea on the screen is updated. The second place is in the parse_sheet function where code to handle a "sheet" line form is added that updates the saved data and Ajax Response textareas. The start_edit routine then renders the sheet using render_sheet, setting the $editmode to "ajax" to have it include a display of the grid. The $extratableattributes argument is set to 'id="sheet0" class="wkcsheet"'. This is needed for the Javascript code to have easy access to that table in the DOM and to help format the table. The $onclickstr is set to q! onclick="rc0('$coord');"! so that clicks on any cell will result in a call to the Javascript rc0 function with an argument containing the cell coordinates. (The render_sheet routine replaces the characters "$coord" in the string with the cell coordinates.) This rendering results in a string with both the CSS style information needed by the sheet ($sheetstyle) and the sheet HTML itself ($sheetstr). The render_values_only routine in Sheet.pm is then used to just render the sheet into a structure with display values and attributes. This structure is then used by the prepare_celldata function in simpleedit1.pl to create the isheet definition for inclusion as part of the Javascript. The Javascript of the libraries, along with the isheet data, is put together with the setting of some Javascript variables and a call to the Edit.js function parse_sheet. This string is then returned to the browser as part of the HTML, along with the $sheetstyle and $sheetstr at appropriate places. The HTML template includes extra CSS needed by the rendered sheet with a grid. It includes attributes in the tag to initialize information needed for re-rendering the sheet and to catch the keyboard events. The HTML for the formula bar includes many
sections that have IDs but are not displayed. These are referenced by some of the Javascript code and need to be there to avoid errors even though some are not used here. The valueedit field and valueedittext