reptile7's JavaScript blog
Monday, March 09, 2020
JavaScript Is the New Excel
Blog Entry #407

Instant spreadsheet grid

Before leaving the College Tuition Chooser behind, I'd like to outline an alternative, JavaScript-based approach to the creation of a spreadsheet template; I'll confine the following discussion to the structure and presentation of the template and leave it to you to functionally kit out the structure as you see fit.

In the fourth part of this series we showed that the chooser's cost values can be added and cleared iteratively. More fundamentally, the chooser <input>s themselves can be created and deployed iteratively - why write out all that HTML if we don't have to?

We'll frame our template with a simple spreadsheetDiv div versus a table.

<form id="spreadsheetForm" name="spreadsheetForm">
...pre-spreadsheet controls...
<div id="spreadsheetDiv"></div>
</form> JavaScript...

The original chooser table layout comprises 11 rows and 4 columns, and can be expanded or shrunk as desired. It would be better if the user could (within reason) set the number of rows and columns from the get-go:

Number of desired input rows: <input name="inputrows" size="5" /><br />
Number of desired input columns: <input name="inputcols" size="5" /><br />

We'll create the template via a createSpreadsheet( ) function that is called by clicking a button.

<button type="button" onclick="createSpreadsheet( );">Create My Spreadsheet</button>

<script type="text/javascript">
var spreadsheetForm = document.getElementById("spreadsheetForm");
var spreadsheetDiv = document.getElementById("spreadsheetDiv");
function createSpreadsheet( ) { ... }
... </script>

The worksheet cells of a standard spreadsheet are indexed with a top-edge row of A, B, C, ... identifiers and a left-edge column of 1, 2, 3, ... identifiers. Let's set the stage for adding these axes to the template by +1-ing the inputrows and inputcols numbers.

var inputrows = Number(spreadsheetForm.elements["inputrows"].value);
var inputcols = Number(spreadsheetForm.elements["inputcols"].value);
var allrows = inputrows + 1;
var allcols = inputcols + 1;

We're ready to create and deploy the template cells and format them a bit: = "nowrap";
for (var i = 0; i < allrows * allcols; i++) {
    var cellInput = document.createElement("input"); = "80px"; = "solid 1px black"; = "0px";
    if (i % allcols == allcols - 1) spreadsheetDiv.appendChild(document.createElement("br")); }

The template rows are terminated not by the right edge of the viewport but by <br> line breaks placed after the i % allcols == allcols - 1 cells, e.g., if allcols is 10, then <br>s are placed after the i = 9, 19, 29, ... cells.

In the name of completeness: Upon setting the cellInput size to 10, bolding the A-Z headers (vide infra) causes the header cell width to shrink slightly with Firefox although not with other browsers, so I changed the size="10" setting to an approximately equivalent width:80px; setting, which is unaffected by the header bolding.

We can respectively populate the top axis's i = 1-26 fields with A-Z headers via:

var colIDs = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; /* This string can be defined globally, prior to the createSpreadsheet( ) function. */
if (1 <= i && i <= allcols - 1) { /* We'll address the colIDs.length < i situation in the next section. */
    cellInput.value = colIDs.charAt(i - 1); }

Alternatively, the fromCharCode( ) method of the String object can be pressed into service for this operation.

cellInput.value = String.fromCharCode(64 + i);
/* The decimal Unicode/ASCII code positions of the uppercase alphabetic characters run from 65 to 90. */

The headers can be bolded, horizontally centered, and readonly-ed in the normal way: = "bold"; = "center";
cellInput.readOnly = true;

We can respectively populate the left axis's i = allcols, allcols × 2, allcols × 3, ... fields with 1, 2, 3, ... headers via:

if (! (i % allcols)) { cellInput.value = i ? i / allcols : ""; }

The chooser's cost fields were given names matching their coordinates: a1, b1, c1, etc. Let's follow suit: = (allcols < i) && (i % allcols) ? colIDs.charAt((i - 1) % allcols) + Math.floor(i / allcols) : cellInput.value;

The header fields are named per their values (A, B, C, ... 1, 2, 3, ...) but as a practical matter those fields don't need to be named at all.


Computer Hope's "How many sheets, rows, and columns can a spreadsheet have?" page reports that the worksheet for recent versions of Excel comprises 1,048,576 rows and 16,384 columns. Do we want to go that big with our template? I don't think so - sounds like a recipe for inducing a hang, if you ask me.

That said, we should be setting the bar higher than a 26-column worksheet.
We can use an

if (colIDs.length < i)
    cellInput.value = colIDs.charAt(Math.floor((i - 1) / colIDs.length - 1)) + colIDs.charAt((i - 1) % colIDs.length);

statement to extend the A-Z headers two-dimensionally à la a standard spreadsheet.

A ... Z AA, AB, ... AZ BA ... BZ ... ZA ... ZZ

This'll give us 702 (26 + 262) columns, which should suffice for most users.

The worksheet for the AppleWorks 5.0 spreadsheet module comprises 500 rows and 40 columns; template creation with these numbers takes about 7 seconds on my computer.

Demo, validation, erasure

I've baked a bit of validation into the demo below.
• Per AppleWorks 5.0, the inputrows number is capped at 500 and the inputcols number is capped at 40 - again, we don't want to cause any unnecessary freezes if we can help it.
• If an inputrows/inputcols value is less than 1 (a blank input is mapped to 0) or is NaN (the input was neither a number nor a blank), then it is set to 1.
• A decimal point-containing inputrows/inputcols number is Math.round( )-ed to the nearest integer.

if (500 < inputrows) { inputrows = 500; spreadsheetForm.inputrows.value = 500; }
if (inputrows < 1 || isNaN(inputrows)) { inputrows = 1; spreadsheetForm.inputrows.value = 1; }
if (/\./.test(inputrows)) { inputrows = Math.round(inputrows); spreadsheetForm.inputrows.value = Math.round(inputrows); } ...

The demo's JavaScript includes a clearAll( ) function that clears the inputrows and inputcols fields and zeroes out the spreadsheet

function clearAll( ) {
    spreadsheetForm.reset( );
    spreadsheetDiv.innerHTML = ""; }
/* In practice, I set the spreadsheetDiv.innerHTML to a set-at-the-outset metatext string - this is a demo, after all... */

and is called by clicking a button.

<button type="button" onclick="clearAll( );">Clear Everything</button>

OK, here we go - check the page source for the full coding.

Number of desired input rows:
Number of desired input columns:

Your spreadsheet template will be generated in this div and below this line of text.

We'll move on to a Super Calculator script in the following entry.

Sunday, February 09, 2020
Approaching Excel, Part 5
Blog Entry #406

Today's post will conclude our College Tuition Chooser discussion by tying up some loose ends and presenting a demo.

Last words on browser support and validation

As noted in the third and fourth parts of this series:
Netscape's JavaScript support began with Navigator 2.0. The chooser can be run by Netscape 2.x upon making some simple changes to the computeForm( ) function and by Netscape 3+ as is, the chooser's if (agentInfo == "Netscape 4.0")-gated JavaScript notwithstanding.

Internet Explorer's JavaScript (more precisely, JScript) support began with IE 3.0. I reported earlier that IE 4.5 handles the chooser's for-Netscape 4.0 code without any problems but I didn't say anything about IE 3 as I didn't have it on my computer at the time. I've recently downloaded IE 3.01 from MacFixer's Vintage Mac Software Library and installed it in the SheepShaver environment. As it happens, IE 3.01 - at least on the Mac side - won't run the chooser because it doesn't respond to change and click events at all - this is not altogether surprising given that onEvent element attributes were not part of HTML 3.2, which became a W3C Recommendation a few months before IE 3.01 for the Mac was released - conversely, Quirksmode's Early event handlers page intimates that IE 3 for Windows users should have been good to go. Anyway...

I previously recommended the use of a regular expression to sort out the chooser's character validation issues. Alternatively and far more simply, we can block non-numberizable cost inputs with the isNaN( ) function:

if (isNaN(input.value)) {
    window.alert("Your input can contain digits and a properly placed decimal point, or be blank; anything else doesn't cut it.");
    window.setTimeout(function ( ) { input.focus( ); ); }, 100); return; }

• Re the above alert( ) message, a blank input is OK - its isNaN( ) return is false - as isNaN( ) maps the empty string to 0.
• A $1,000 input is not OK as its $ and comma characters prevent its conversion to a number; however, you may prefer that the user keep those characters out of the input in the first place.
• Additional code would be needed to flag a number with a fractional part running past the hundredths place or a negative number.

Regardless of what we do or don't do validation-wise, it would be a good idea to preface the chooser with some metatext that specifies what the inputs can and can't be, and I'll do that for the demo.

Average it

In my former life as an instructor of organic chemistry*, I used spreadsheets to calculate exam averages as part of my regular record-keeping activities, and it occurred to me that adding an averaging capability to the chooser would be a useful thing to do.
*Some of my teaching materials from back in the day can still be downloaded as .pdf files here.

We can output averages for the various cost types to the cells of a new, fifth column on the right side of the chooser.

<tr><th>Name of Institution</th> ... <th>Average</th></tr>
<tr><th>Tuition</th> ... <td><input type="text" name="d1" size="15"></td></tr>
<tr><th>Fees</th> ... <td><input type="text" name="d2" size="15"></td></tr>
<tr><th>Total Cost of Attendance</th> ... <td><input type="text" name="d9" size="15"></td></tr>

The getAverage( ) function below will add up and average one row of costs à la a standard spreadsheet:

function getAverage(input) {
    var columnID = new Array("a", "b", "c");
    var rowID =;
    var nonblank = 0;
    var runningfieldtotal = 0;
    for (var i = 0; i < columnID.length; i++) {
        if (input.form.elements[columnID[i] + rowID].value) {
            runningfieldtotal += Number(input.form.elements[columnID[i] + rowID].value); } }
    var rowaverage = runningfieldtotal / nonblank;
    input.form.elements["d" + rowID].value = rowaverage.toFixed(2); }

function calc(input) {
    if (validateData(input)) {
        getAverage(input); } }

I didn't do any play-by-play for the modified computeForm( ) functions in the previous post, so let me do so in this case.

We set up a columnID Array of the a and b and c column identifiers.

We use the name property of the Text object and the charAt( ) method of the String object to identify (rowID) the row that holds the onchanged input.

We'll count the number of nonblank fields in the row and store that number in a nonblank variable. The division operation that computes the average should exclude blank fields: 10000 and 8000 and a blank should give a 9000 average and not a 6000 average.

We for-iterate across the columnID columns: if a columnID[i] + rowID field is nonblank, then the nonblank count is increased by one and the field's value is Number( )-ized and the resulting number is added to a runningfieldtotal.

When the loop has run its course, the runningfieldtotal is divided by the nonblank count to give the rowaverage.

The rowaverage is toFixed(2) to the hundredths place** whether it reaches that place or not, e.g., a 9000 average becomes 9000.00; the formatted average is displayed in the fifth column's d + rowID field.
**If you would rather the average be a whole number, then you can Math.round( ) the rowaverage instead.

Like the computeForm( )getTotal( ) function, the getAverage( ) function is called from the calc( ) function after the input.value has passed validation.

Vertical tabbing, take one

With some spreadsheet applications, e.g., the AppleWorks spreadsheet module I used way back when, pressing the Enter/Return key effects a vertical tabulation from the current cell to the cell directly below it. I like this feature, so I'm going to add it to the chooser, as follows:

<form name="costsForm" onsubmit="return false;"> ...chooser form element content... </form>
<script type="text/javascript">
function tabdown(e) {
    var columnID, rowID, thisKey;
    if (e) {
        columnID =;
        rowID =;
        thisKey = e.key ? e.key : e.which; }
    else if (window.event) {
        columnID =;
        rowID =;
        thisKey = window.event.keyCode; }
    else {
        window.alert("Sorry, your browser does not support the event object.");
        return; }
    if (thisKey == "Enter" || thisKey == 13) // If the user presses the Enter/Return key:
        document.forms["costsForm"].elements[columnID + (Number(rowID) + 1)].focus( ); }
for (var i = 0; i < document.forms["costsForm"].elements.length - 2; i++)
    document.forms["costsForm"].elements[i].onkeydown = tabdown;

The above code accommodates the Netscape event model and the Internet Explorer event model. Yes, I know that Microsoft support for the Netscape event model dates to IE 9 and that the window.event object now has a legacy status, but so what? Suppose that 0.01% of the world's online population are browsing with IE 5.5-8: that's 100 people per million users, and if I can bring them into the loop with a modicum of effort, then why shouldn't I do so?

A tabdown( ) function that listens for keydown events is iteratively registered on all of the chooser form's controls except the and buttons.

When a keydown event occurs in a chooser field, the tabdown( ) function uses the target or srcElement property of the keydown event object to access the field and uses name.charAt( ) operations to get the field's columnID and rowID coordinates. Subsequently, tabdown( ) uses the key, which, or keyCode property of the keydown event object to determine which key was depressed.

The srcElement, which, and keyCode properties are deprecated but are nonetheless supported by the current versions of all of the major browsers, including Firefox if they are yoked to an e-type event object.

If the depressed key was the Enter/Return key, then e.key returns Enter whereas e.which and window.event.keyCode return 13, and if that's what we've got, then focus( ) is sent to the columnID column's Number(rowID) + 1 field.

The i count includes the Name of Institution fields, which can get in on the vertical tabbing action if their title1, title2, and title3 names are changed to a0, b0, and c0, respectively.

The OpenOffice spreadsheet module's Enter/Return behavior is a bit more complicated.
One or more horizontal tabulations followed by an Enter/Return keypress gives a newline-like transfer of focus (A1 B1 C1 A2).
In the absence of prior horizontal tabbing, OpenOffice behaves like AppleWorks does (A1 A2).
Can we reproduce this behavior JavaScriptically? Maybe, I'd have to give it some thought...

There's more to the spreadsheet tabulation topic that we could discuss - you may know that pressing Shift + Tab effects a leftward tab and that pressing Shift + Enter/Return effects an upward tab - oh, and what about the ➡ and ⬇ and ⬅ and ⬆ arrow keys? - but let's move on.

A new interface?

As detailed previously, a standard spreadsheet requires the user to put some sort of formula in an output cell. Once that formula is in place, the spreadsheet's I/O action parallels that of the chooser, i.e., onchange-ing a formula input value returns automatically a new formula output value.

The chooser puts its addition formulas in the computeForm( ) function; the user doesn't see or handle these formulas at all, which initially rubbed me the wrong way - I felt that this lessened the user's 'conscious agency' in adding up the cost values, shall we say. I gave some thought to getting rid of all those onchange="calc(this);" attributes and instead using and buttons to call functions that would calculate respectively all of the column totals and all of the row averages at once - clicking a push button is the normal JavaScript way of setting something in motion, after all - but ultimately decided that it would actually be kind of annoying to have to click those buttons over and over again.

Perhaps we could make the chooser arithmetic a little less obscure by setting the defaultValues of the Total Cost of Attendance and Average fields to standard formulas - =SUM(A1:A8), =AVERAGE(A1:C1), etc. - or would this just clutter up the display? What do you think?


The demo of this section is a work in progress, but that's true of all creative efforts, isn't it? The demo incorporates the bulk of the new coding I've put forward in the last several entries - check the page source for all the details from start to finish.

Addition is carried out by the One column at a time computeForm( )getTotal( ) functionality. Averaging is carried out by the getAverage( ) function given above; I've appended a getAverage(input.form.elements[columnID + 9]); call to the getTotal( ) function body so that the Total Cost of Attendance row is averaged along with the input row in question. Validation is carried out by a new validateData( ) function containing the Segue to regular expressions statements. The Back to the beginning resetting facility has been deployed mostly as is - there are two minor edits thereto.

Per the limitations of Blogger, all styles are effected JavaScriptically. Moreover, the Total Cost of Attendance and Average fields have been made readonly, per my preference.

var costsForm = document.forms["costsForm"];
for (var i = 0; i < costsForm.elements.length - 2; i++) {
    costsForm.elements[i].style.textAlign = i < 3 ? "center" : "right";
    if (i % 4 == 2 || i > 34) costsForm.elements[i].readOnly = true; }

Lastly, the streamlined calc( ) coordinator is bound to the cost input fields JavaScriptically so as to tidy up the HTML a bit more.

for (var i = 3; i < costsForm.elements.length - 6; i++) {
    if (i % 4 == 2) continue;
    costsForm.elements[i].onchange = function ( ) { calc(this); } }

OK, here we go. The following spreadsheet can be used to sum and average costs automatically for three institutions of learning.
Your cost inputs may contain
digits running from the hundred-thousands place to the hundredths place,
a starting $,
a comma between a thousands place digit and a hundreds place digit, and
a decimal point between the ones place digit and a tenths place digit;
nonconforming inputs will be intercepted.

Institutional Cost of Attendance Worksheet
Name of Institution Average
Total Cost of Attendance

Tuesday, December 31, 2019
Adding and Clearing Columns of Numbers
Blog Entry #405

We've got two more College Tuition Chooser functions to put under the microscope, they being computeForm( ) and formClear( ). Before we get started, however, let's review a bit...

Calling the computeForm( ) function

Like the validateData( ) function, the computeForm( ) function is called from the calc( ) function, which is redetailed below in its entirety.

function calc(input) {
    if (agentInfo == "Netscape 4.0") {
        input.value = validateData(input.value);
        computeForm(input.form); }
    else {
        if (validateData(input.value)) computeForm(input.form);
        else {
            window.alert("Please do not use any characters besides numbers on this line. Your entry will be erased.");
            input.value = 0; } } }

For Netscape 4.0x users, every calc(this) call ultimately effects a computeForm(input.form) call. There are no input.values for which this does not happen: if the user's a1 input is hello world, then the validateData( ) function's correctedNum number stays at 0, which is returned to the calc( ) function and then passed on to the computeForm( ) function. The validateData( ) function does discard arithmetically unacceptable input characters but in no cases is the user alert( )ed that a given input itself may be unacceptable.

For non-Netscape 4.0x users, a calc(this) call effects a computeForm(input.form) call only if the input.value contains only digit and decimal point characters or is blank. When the not-for-Netscape 4.0 part of the validateData( ) function runs into an unapproved ch character, the not-for-Netscape 4.0 part of the calc( ) function alert( )s the user accordingly and sets the input.value to 0, and no further processing occurs.

Taking everything into account, the not-for-Netscape 4.0 approach is in my book better than the for-Netscape 4.0 approach: a problematic input should be blocked (if not erased) before any addition gets under way.

Adding it all up

Suppose we want to add a series of eight numbers via an OpenOffice spreadsheet.
We type the numbers individually into the spreadsheet's A1...A8 cells and then give focus to the A9 cell.
We access the spreadsheet's Insert menu → Function... Wizard.
We select the SUM option from the wizard's FunctionsFunction menu.
We click the button at the bottom of the wizard to input an =SUM( ) pre-formula into the wizard's Formula box.
We plug an A1:A8 parameter range into the =SUM( ) pre-formula.
We click the wizard's button to post the completed =SUM(A1:A8) formula and resulting sum to the A9 cell.

The College Tuition Chooser doesn't wait for a full column of costs to get summing, however. Per the above calc( ) code, each post-validation input.value fires the computeForm( ) function, which discretely sums the cost values for the a# and b# and c# columns and outputs the totals in the a9 and b9 and c9 fields, respectively.

function computeForm(form) {
    form.a9.value = form.a1.value * 1 + form.a2.value * 1 + form.a3.value * 1 + form.a4.value * 1 + form.a5.value * 1 + form.a6.value * 1 + form.a7.value * 1 + form.a8.value * 1;
    form.b9.value = form.b1.value * 1 + form.b2.value * 1 + form.b3.value * 1 + form.b4.value * 1 + form.b5.value * 1 + form.b6.value * 1 + form.b7.value * 1 + form.b8.value * 1;
    form.c9.value = form.c1.value * 1 + form.c2.value * 1 + form.c3.value * 1 + form.c4.value * 1 + form.c5.value * 1 + form.c6.value * 1 + form.c7.value * 1 + form.c8.value * 1; }

The various values are numberized for arithmetic addition via * 1 multiplications (otherwise they'd be concatenated); importantly, blanks are mapped to 0s for those multiplications by almost all browsers. A 10000 a1 input therefore gives a 10000 a9 output but it also gives a 0 b9 output and a 0 c9 output.

Total Cost of Attendance$$$

The calc( )/computeForm( ) functionality updates the a9 and b9 and c9 values automatically as the user fills in the chooser form.

The computeForm( ) statements clash with my organizational bent. Why are we writing out all those terms? I like to do things iteratively whenever possible, so here's how I would carry out the addition:

var columnID = new Array("a", "b", "c"); /* Netscape 3.x doesn't support array literals. */
for (var i = 0; i < columnID.length; i++) {
    var runningfieldtotal = 0;
    for (var j = 1; j < 9; j++)
        runningfieldtotal += Number(form.elements[columnID[i] + j].value); /* Number("") returns 0. */
    form.elements[columnID[i] + 9].value = runningfieldtotal; }

Moreover, an array-loop approach would make it much easier to scale the addition if we were to add more columns and rows to the chooser.

Netscape 2.x and the computeForm( ) function

Navigator 2.0 does not map blanks to 0s in the computeForm( ) function but instead throws a value is not a numeric literal error as soon as it hits a "" * 1 multiplication - at least that's what I see on my iMac. I'm sure that some people were still using Netscape 2.x when the chooser went live, and those users could have been accommodated by the preceding for-based addition code upon making a few simple modifications thereto:
(1) Use an Object object to array the a and b and c column identifiers.
(2) Explicitly convert blanks to 0s before attempting to add them.
(3) Numberize the cost values with the eval( ) function.

var columnID = new Object( ); /* Netscape 2.x doesn't support the Array object at all. */
columnID[0] = "a";
columnID[1] = "b";
columnID[2] = "c";
columnID.length = 3;
for (var i = 0; i < columnID.length; i++) {
    var runningfieldtotal = 0;
    for (var j = 1; j < 9; j++) {
        if (! form.elements[columnID[i] + j].value.length) form.elements[columnID[i] + j].value = 0;
        runningfieldtotal += eval(form.elements[columnID[i] + j].value); } /* eval("") returns undefined. */
    form.elements[columnID[i] + 9].value = runningfieldtotal; }

Actually, there's no reason why we couldn't use this code with modern browsers; re the danger of using eval( ), malicious code inputted by a malicious user would be blocked at the validation stage.

The form.elements[columnID[i] + j].value = 0 assignments load 0s into the rows[1]-rows[8] non-input cost fields; if you don't like that (I don't care for it myself), then you can blank those fields with:

var form2 = null;
function computeForm(form) {
    ...addition code...
    form2 = form;
    window.setTimeout("for (var i = 3; i < 27; i++) if (form2.elements[i].value == '0') form2.elements[i].value = '';", 10); }

One column at a time

If you would prefer to add up just one column of costs at a time à la a standard spreadsheet, then that's pretty easy to arrange:

function computeForm(input) {
    var columnID =;
    var runningfieldtotal = 0;
    for (var i = 1; i < 9; i++)
        runningfieldtotal += Number(input.form.elements[columnID + i].value);
    input.form.elements[columnID + 9].value = runningfieldtotal; }

/* In the calc( ) function, if the input.value passes a regexp validation or is blank: */
if (cost_pattern.test(input.value) || ! input.value.length)

I trust you are up to the task of modifying this computeForm( ) version for all of the Netscape 2.x users out there. ;-)

Back to the beginning

The chooser's formClear( ) function empties all of the cost fields, including the Total Cost of Attendance fields, and is called by clicking the button at the bottom of the display.

function formClear(form) {
    form.a1.value = ""; form.a2.value = ""; form.a3.value = ""; form.a4.value = "";
    form.a5.value = ""; form.a6.value = ""; form.a7.value = ""; form.a8.value = "";
    form.a9.value = ""; form.b1.value = ""; form.b2.value = ""; form.b3.value = "";
    form.b4.value = ""; form.b5.value = ""; form.b6.value = ""; form.b7.value = "";
    form.b8.value = ""; form.b9.value = ""; form.c1.value = ""; form.c2.value = "";
    form.c3.value = ""; form.c4.value = ""; form.c5.value = ""; form.c6.value = "";
    form.c7.value = ""; form.c8.value = ""; form.c9.value = ""; }

Again, there is no need to process the fields individually when we can do so iteratively, in this case with just one line of code:

for (var i = 3; i < 30; i++) form.elements[i].value = "";

To clear the Name of Institution fields as well, we can supplement the button with a traditional reset button:

<tr><td id="resetTd" colspan="4">
<input type="button" value="Just Clear the Numbers" onclick="formClear(this.form);">
<input type="reset" value="Clear All Fields">

We're almost ready to wrap up our College Tuition Chooser discourse. I would still like to add an =AVERAGE(number1, number2, ...) capability to the chooser and then provide a demo along with some summary commentary, and I'll do so in the following entry; depending on how long that takes, I may also discuss an alternative, JavaScript-based approach to the creation of a spreadsheet template that I've been working on.

Monday, December 09, 2019
More Tuition Check
Blog Entry #404

Let's get back now to our ongoing analysis of the JavaScript/Java Goodies College Tuition Chooser and its calc( )/validateData( )/pow( ) input-processing functionality.

Original processing issues, in execution order

The College Tuition Chooser was written during the JavaScript 1.2 → Navigator 4.0-4.05 era and the calc( ) and validateData( ) functions both feature an if (agentInfo == "Netscape 4.0") { ... } block. No features in those blocks were implemented in JavaScript 1.2 or are Netscape-specific, however. Upon switching the agentInfo == "Netscape 4.0" conditions to agentInfo != "Netscape 4.0" conditions, Navigator 3.04 and IE 4.5 run the blocks and then the computeForm( ) function without incident. Even Navigator 2.0, the first browser with a JavaScript engine, smoothly handles the validateData( ) for-Netscape 4.0 code although this version does throw a value is not a numeric literal error at the computeForm( ) stage, which we will discuss later.

Regarding non-digit ch characters, the validateData( ) for-Netscape 4.0 block allows the presence of decimal points and commas and $s whereas the following not-for-Netscape 4.0 else { ... } block allows the presence of only decimal points: this is fine for our $12,34r.67 example but we'll have to up our game if we want to stop inputs like ..1.23 and ,45,,6,,, and 7$8$9.

Every non-digit character in the initial input.value string is removed by the validateData( ) for-Netscape 4.0 block, which is fine for a correctly placed $ or comma or decimal point but definitely causes a problem vis-à-vis the $12,34r.67 value's mistyped r, whose removal prevents the integer digits from reaching their original place-value positions - the 1 stops at the thousands place versus the ten-thousands place, the 2 stops at the hundreds place versus the thousands place, etc. - so that the validateData( ) correctedNum return, 1234.67, is smaller than the starting input by a factor of 10! We can minimize the damage in this case by mapping the r to a 0:

else if ((ch != ",") && (ch != "$")) {
    window.alert("The invalid character " + ch + " was detected and will be replaced by a 0.");
    if (! isDecimal) correctedNum = Number(correctedNum + "0");
    else { addOn += "0"; ++decimalPlace; } }
/* The Number( ) string-to-number conversion works for Netscape 3+; for Netscape 2, eval( ) would work. */

In contrast, the not-for-Netscape 4.0 code converts the initial input.value to 0 when it hits a not-OKed ch character, which would be fair enough for a hello world input but a heavy-handed response for a 1,000 input - I prefer in this case to return focus( ) to the input and to leave in place and select( ) the value:

/* At the end of the calc( ) function, after validateData( ) has returned false: */
else {
    window.alert("Your input may contain digits and a decimal point: please remove any other characters.");
    window.setTimeout(function ( ) { input.focus( ); ); }, 100); }
/* The loss-of-focus aspect of the onchange="calc(this);" trigger can interfere with the focus( ) and select( ) operations but that interference can be overcome by a short setTimeout( ) delay. */

The pow( ) function generates a 10decimalPlace power of 10 for the division operation that fractionizes the addOn part of the input; however, there's no need to create an ad hoc function when we can avail ourselves of the pow( ) method of the Math object for this purpose:

if (decimalPlace > 0) correctedNum += addOn / Math.pow(10, decimalPlace);

The validateData( )/pow( ) code places no limits on the number of post-decimal point digits: we could input 3.141592653589793 and the whole thing would be carried forward. However, it is simple enough to truncate such a too_long str string at the 'thousandths' place and penultimately round the final correctedNum number at the hundredths place:

var str = theNum;
var decimalpointindex = str.indexOf(".");
if (decimalpointindex != -1 && decimalpointindex + 2 < str.length) {
    var too_long = true;
    str = str.substring(0, str.indexOf(".") + 4); }
if (too_long) correctedNum = Math.round(correctedNum * 100) / 100;
// More modernly: if (too_long) correctedNum = correctedNum.toFixed(2);
return correctedNum;

One nonetheless wonders, "Why have different ways of dealing with the integer and fractional parts of an input? Just recast the str string as a new well-formed numeric str2 string containing only digits and a decimal point, and then Number( )-ize str2 at the end. What's the problem?"

Segue to regular expressions

If we do want to do something special for level 4 (and later) browsers versus their predecessors, then we can vet the cost inputs via a suitable regular expression: regular expressions were implemented in JavaScript 1.2.

// Begin the calc( ) body with:
var cost_pattern = /^\$?(\d{1,6}|\d{1,3},\d{3})(\.\d{0,2})?$/;
if (cost_pattern.test(input.value)) { ... computeForm(input.form); }
else {
    window.alert("Houston, we have a problem - please get the offending character(s) out of your input.");
    window.setTimeout(function ( ) { input.focus( ); ); }, 100);
    return; }

The cost_pattern pattern allows the presence of
a starting $ and
digits running from the hundred-thousands place* to the hundredths place and
a comma between a thousands place digit and a hundreds place digit and
a decimal point between the ones place digit and a tenths place digit
in the input.value.
Everything else ist verboten, ja?
*The annual tuition cost for some dental schools clears six figures.

For arithmetic purposes the inputs cannot contain a $ or a comma, but we can easily track down these characters and replace( ) them with empty strings via the following lines of regular expression-based code:

var charOK = /[$,]/g;
input.value = input.value.replace(charOK, "");

We presented a similar approach to the removal of a document's HTML tags in the A non-iterative 'detagification' section of Blog Entry #98.

Other code possibilities that prevent Netscape 2.x-3.x errors, in the name of completeness

Navigator 3.04 and Navigator 2.0 throw a syntax error as soon as they hit the
var cost_pattern = /^\$?(\d{1,6}|\d{1,3},\d{3})(\.\d{0,2})?$/; line,
even if that line is in the body of a not-called function or in a not-operative if or else block: we can suppress this error if we formulate the cost_pattern pattern via the RegExp object constructor

var cost_pattern2 = new RegExp("^\\$?(\\d{1,6}|\\d{1,3},\\d{3})(\\.\\d{0,2})?$");

and shield the regular expression code with an if (input.value.replace) { ... } gate.
Like regular expressions themselves, the replace( ) method of the String object was implemented in JavaScript 1.2.

I should also note that Navigator 3.04 and Navigator 2.0 similarly throw a syntax error as soon as they hit the window.setTimeout(function ( ) { input.focus( ); ); }, 100); line:
we can suppress this error if we
define a global var input2 = null; variable,
assign input2 = input; in the calc( ) body, and
recast the focus( )/select( ) action as window.setTimeout("input2.focus( ); );", 100);.

Mozilla now discommends the setTimeout(codeString, delay) syntax, but again, we are accommodating late-1990s Netscape 2.x-3.x users here. I have no idea why the calc( ) and setTimeout( ) execution contexts converge in one case and diverge in the other.

Other inputs

For previous CCC applications we have generally intercepted blank, 0, and negative number Text.values.

A negative number is converted to its additive inverse by the for-Netscape 4.0 part of the validateData( ) function but is blocked - as it should be - by the not-for-Netscape 4.0 part and by my if (cost_pattern.test(input.value)) gate.

A 0 input is of course OK; a blank input is a 0-equivalent in an arithmetic context and is also OK, as it would be for a standard spreadsheet. The onchange="calc(this);" event handler is not triggered by leaving a field blank in the first place, although it would be triggered if we were to clear a non-blank field and then blur the field. The original chooser code handles the latter case satisfactorily:

(NN4.0) With Netscape 4.0x browsers, the now-empty input.value is mapped to 0 by the relevant parts of the validateData( ) and calc( ) functions, and we actually get a 0 in the field on the page when the dust has settled.

(!NN4.0) With non-Netscape 4.0x browsers, the input.value passes through the relevant parts of the calc( ) and validateData( ) functions without modification and is in almost all cases subsequently converted to 0 at the level of code but not on the page by the computeForm( ) function.

An empty input.value would be blocked by the if (cost_pattern.test(input.value)) gate: adding an || ! input.value.length subcondition to the gate will let it through.
We'll check over and retool the chooser's computeForm( ) and formClear( ) functions in the following entry.

Tuesday, November 12, 2019
Tuition Check
Blog Entry #403

We are at present discussing the JavaScript/Java Goodies College Tuition Chooser. We worked through the chooser's structure and presentational aspects in our last episode and are now ready to tackle the <script> code in the document head. The chooser JavaScript features five functions:
(1) A validateData( ) function vets the user's inputted cost values.
(2) For Netscape 4.0x users, a pow( ) function assists the validateData( ) function in dealing with the fractional part(s) of any cost values that have not been rounded to whole dollars.
(3) A computeForm( ) function adds up each column's cost values and displays the results in the Total Cost of Attendance fields in the tenth row.
(4) A calc( ) function serves as a gateway to, and coordinates, the validateData( )/computeForm( ) action.
(5) A formClear( ) function blanks all of the cost fields.
We'll cover the calc( ), validateData( ), and pow( ) functions in today's post.

So, suppose that Somewhere State University is on a shortlist of schools that Jane User is thinking of applying to. Jane calls up the College Tuition Chooser and attempts to type Somewhere State University into the first row's
<input type="text" name="title1" maxlength="20" size="15"> field
but only gets as far as the v because of the field's maxlength setting, so she uses an SSU abbreviation for the institution name. Jane knows from some online research that SSU's current annual tuition cost is $12,345.67, which she mistypes as $12,34r.67 in the second row's
<input type="text" name="a1" onchange="calc(this);" size="15"> field.
Jane clicks on the third row's
<input type="text" name="a2" onchange="calc(this);" size="15"> field,
thereby triggering the a1 field's onchange event handler, which calls the calc( ) function and passes thereto a this object reference to the a1 field.

function calc(input) { ... }

The a1 Text object is at the outset given an input identifier. What happens next depends on whether Jane is browsing with Netscape 4.0x or with some other browser.

if (agentInfo == "Netscape 4.0") { ... }

The agentInfo variable is defined prior to the five functions by:

var userAgent = navigator.appName + " " + navigator.appVersion;
var agentInfo = userAgent.substring(0, 12);

My SheepShaver browsers include Navigator 4.05, for which
navigator.appName returns Netscape and
navigator.appVersion returns 4.05 (Macintosh; I; 68K, Nav) and therefore
agentInfo is Netscape 4.0 - it checks out.

Let's assume, then, that it's October 1997 and that
Jane is as a matter of course browsing with Navigator 4.03 and therefore goes through the if (agentInfo == "Netscape 4.0") gate.
Subsequently, the calc( ) function calls the validateData( ) function and passes thereto the input.value, $12,34r.67.

input.value = validateData(input.value);

In this example, the validateData( ) function will convert the $12,34r.67 string to a 1234.67 number - not really a desired outcome, but that's what happens. The $12,34r.67 argument is given at first a theNum identifier and then a str identifier:

function validateData(theNum) {
    var str = theNum;

The rest of the validateData( ) function comprises an if...else statement whose if and else blocks serve Netscape 4.0x users and other browser users, respectively. The if block and its pow( ) function assistant are detailed below:

if (agentInfo == "Netscape 4.0") {
    var correctedNum = 0;
    var decimalPlace = 0;
    var addOn = "0";
    var isDecimal = false;
    for (var i = 0; i < str.length; i++) {
        var ch = str.substring(i, i + 1);
        if (((ch >= "0") && (ch <= "9")) && (! isDecimal)) {
            correctedNum *= 10;
            correctedNum += ch * 1; }
        else if (((ch >= "0") && (ch <= "9")) && (isDecimal)) {
            addOn += ch;
            ++decimalPlace; }
        else if (ch == ".") isDecimal = true;
        else if ((ch != ",") && (ch != "$")) window.alert("The invalid character " + ch + " was detected and ignored."); }
    if (decimalPlace > 0) correctedNum += addOn / pow(decimalPlace);
    return correctedNum; }

function pow(exp) {
    var returnTotal = 10;
    for (var j = 1; j < exp; j++) returnTotal *= 10;
    return returnTotal; }

Jane goes through the if (agentInfo == "Netscape 4.0") gate. Local correctedNum, decimalPlace, addOn, and isDecimal variables are initialized to the number 0, the number 0, a "0" string, and a boolean false, respectively. A for statement then checks over the str characters and transfers str's pre-decimal point digits to the correctedNum variable and its post-decimal point digits to the addOn variable - here's a rundown of what happens:

i = 0
The str.substring(i, i + 1) operation gets the zeroth str character, $, which is assigned to a ch variable. The ch $ doesn't go through any of the if...else if...else if...else if gates, and nothing is done with it. Regarding the ((ch >= "0") && (ch <= "9")) comparisons, note that $ lies outside of the 0-9 ASCII code position range; ditto for str's comma, r, and decimal point characters.

i = 1
The next str character, 1, is assigned to ch. The ch 1 goes through the if (((ch >= "0") && (ch <= "9")) && (! isDecimal)) gate and is then numberified by a * 1 multiplication and the resulting 1 number is added to correctedNum (01).

i = 2
The next str/ch character, 2, goes through the if (((ch >= "0") && (ch <= "9")) && (! isDecimal)) gate. The correctedNum *= 10; statement left correctedNum at 0 in the previous iteration; in this iteration, the statement's * 10 multiplication takes correctedNum's 1 to 10. The ch 2 is numberified and added to correctedNum (1012).

i = 3
The next str/ch character is the comma, which doesn't go through any of the if/else if gates, and nothing is done with it.

i = 4
The next str/ch character, 3, goes through the if (((ch >= "0") && (ch <= "9")) && (! isDecimal)) gate; the correctedNum *= 10; statement takes correctedNum's 12 to 120; the ch 3 is numberified and added to correctedNum (120123).

i = 5
The next str/ch character, 4, goes through the if (((ch >= "0") && (ch <= "9")) && (! isDecimal)) gate; the correctedNum *= 10; statement takes correctedNum's 123 to 1230; the ch 4 is numberified and added to correctedNum (12301234).

i = 6
The next str/ch character, r, goes through the else if ((ch != ",") && (ch != "$")) gate; Jane is alert( )ed that The invalid character r was detected and ignored., and she gives her .

i = 7
The next str/ch character is the decimal point, which goes through the else if (ch == ".") gate; the isDecimal boolean is toggled to true.

i = 8
The next str/ch character, 6, goes through the else if (((ch >= "0") && (ch <= "9")) && (isDecimal)) gate; the ch 6 is appended to addOn to give a 06 string; decimalPlace is incremented to 1.

i = 9
The last str/ch character, 7, goes through the else if (((ch >= "0") && (ch <= "9")) && (isDecimal)) gate; the ch 7 is appended to addOn to give a 067 string; decimalPlace is incremented to 2.

That's it for the loop. Control passes to the if (decimalPlace > 0) correctedNum += addOn / pow(decimalPlace); conditional, whose condition returns true and which pieces together the correctedNum and addOn data.

The if statement first calls the pow( ) function and passes thereto decimalPlace, which is given an exp identifier. The pow( ) function initializes a local returnTotal variable to 10 and then takes returnTotal to 100 via the * 10 multiplication of a one-iteration for (var j = 1; j < exp; j++) returnTotal *= 10; statement.

The exponentiated returnTotal is returned to the addOn / pow(decimalPlace) division, which gives the number 0.67, which is added to correctedNum to give 1234.67, which is returned to the calc( ) function, where it is assigned to the input.value.

The input.value = validateData(input.value); assignment is followed by a computeForm(input.form); call; we will discuss the computeForm( ) function in the following post.

Of course, Jane may not be using a Netscape 4.0x browser: she may be using Internet Explorer 4.0 or perhaps an earlier version of Netscape, or maybe it's not 1997 but it's 2019 and she's using a modern browser. In this case, she moves past the calc( ) function's if (agentInfo == "Netscape 4.0") block to a complementary else block, which itself initially calls and passes the input.value to the validateData( ) function:

else {
    if (validateData(input.value)) computeForm(input.form);

At the validateData( ) function, Jane moves past the if (agentInfo == "Netscape 4.0") block to a much more basic else block that also checks over the str characters but returns true if each character is either a digit or a decimal point or false if it runs into any other type of character.

else {
    for (var z = 0; z < str.length; z++) {
        var ch = str.substring(z, z + 1);
        if (((ch < "0") || (ch > "9")) && (ch != ".")) return false; }
    return true; } } // End of the validateData( ) function

A true return effects a computeForm(input.form) call. A false return pops up an "only numbers, please" alert( ) and sets the input.value to 0, which for $12,34r.67 happens as soon as we hit the $.

else {
    window.alert("Please do not use any characters besides numbers on this line. Your entry will be erased.");
    input.value = 0; } } // End of calc( )'s not-for-Netscape 4.0x else block

There are a number of things that we need to note about the above validation code, but those things can wait until the next entry.

Tuesday, October 22, 2019
Approaching Excel, Part 1
Blog Entry #402

All right, in today's post we'll begin a look at the CCC sector's College Tuition Chooser, which was created by Brian O'Keefe and went live in October 1997. The College Tuition Chooser codes a simple spreadsheet that stores and sums the various costs of attending 1-3 colleges or other institutions of learning.

The tuition.html page features a demo that works pretty well granted that
a large chunk of the chooser's JavaScript is meant for Netscape 4.0x and
the chooser's on-the-fly onchange addition action is fundamentally different from the =SUM(number1, number2, ...) formula input you'd use with a standard spreadsheet.
The chooser code may be accessed and saved at the corresponding tuition.txt page.

Layout and initial content

The chooser architecture commingles
a table having 11 rows and 41 cells and
a form holding 30 text inputs and a reset button.

<table cellspacing="0" cellpadding="0" width="100%" border="3"> ...
<form method="post" onsubmit="return false;"> ...

A form element can validly contain a table element but the converse is not true, and therefore the above form-in-table nesting should be inverted to a table-in-form nesting.

The cellspacing, cellpadding, and width attributes of the table element are entirely obsolete; the border attribute of the table element can be validly set to "" (the empty string) or 1 but is obsolete if set to any other value.

We won't be submitting any data to a processing agent, so the form element's method and onsubmit attributes should be removable. However, upon subtracting the onsubmit attribute and when using late-1990s versions of Internet Explorer,
clicking the enter/return key
after entering a value into a chooser text field
triggers a form submission attempt* that subsequently throws a The attempt to load 'pathname/tuition.html' has failed.-type error - at least that's what I see with IE 4.5 and IE 5.1.7 in the SheepShaver environment.
(*Our study of HTML Goodies' "Submit the Form Using Enter" tutorial made me suspect that something like this might happen.)
I have no idea if this occurs with Microsoft's recent Edge/IE browsers (I don't see it with Navigator 4.05, Communicator 4.61, or any other browser on my computer): in any case, I reckon there's no harm in keeping the submit cancelation to be on the safe side, but yes, we can at least lose the method setting.

At the top of the table we've got an Institutional Cost of Attendance Worksheet caption.

<table ...>
<caption><h3>Institutional Cost of Attendance Worksheet</h3></caption>

As shown, the caption text is also marked up with an h3 element, which would not have been valid in HTML 3.2 (the current version of HTML in October 1997) but is OK in HTML 5.

Each table row holds four cells except for the last row, which holds one cell. The table's first column contains a preset series of college cost headers (not marked up as such):
Name of Institution, Tuition, Fees, Books/Supplies, Board, Room, Personal/Recreation, Transportation, Other, Total Cost of Attendance

<tr><td>Name of Institution</td> ... </tr>
<tr><td>Tuition</td> ... </tr>
<tr><td>Fees</td> ... </tr> <!-- Etc. -->

The table's second, third, and fourth columns contain text boxes for entering values for the headers to their left.

<!-- 1st row, for the names of the institutions that you're thinking of applying to: -->
<td align="center">1.<input type="text" name="title1" maxlength="20" size="15"></td>
<td align="center">2.<input type="text" name="title2" maxlength="20" size="15"></td>
<td align="center">3.<input type="text" name="title3" maxlength="20" size="15"></td>
<!-- 2nd row, for the tuitions those institutions charge: -->
<td align="center">$<input type="text" name="a1" onchange="calc(this);" size="15"></td>
<td align="center">$<input type="text" name="b1" onchange="calc(this);" size="15"></td>
<td align="center">$<input type="text" name="c1" onchange="calc(this);" size="15"></td>
<!-- 3rd row, for the institutions' miscellaneous fees: -->
<td align="center"> &nbsp;&nbsp;<input type="text" name="a2" onchange="calc(this);" size="15"></td>
<td align="center"> &nbsp;&nbsp;<input type="text" name="b2" onchange="calc(this);" size="15"></td>
<td align="center"> &nbsp;&nbsp;<input type="text" name="c2" onchange="calc(this);" size="15"></td> <!-- Etc. -->

All of the input cells have an align="center" attribute that horizontally centers their contents and is now obsolete. All of the input text boxes have a size="15" attribute that effectively sets the width of the boxes and is still valid.

The table's last row holds the aforementioned reset button.

<tr><td colspan="4"><div align="center"><input type="button" value="RESET" onclick="formClear(this.form);" size="150"></div></td></tr>

Do we really need to put the button in a <div align="center"> in order to horizontally center it in the rows[10].cells[0] cell? I trust y'all know the answer to that one. The size="150" setting has no effect on the button width.

Style matters

My my, look at all of the W3C's recent CSS activity...

The table itself

The cellspacing="0" attribute maps onto a table { border-collapse: collapse; } style rule, which in the present case not only collapses td td and td table interspace but also shrinks the table/td border-widths by a pixel.

The cellpadding="0" attribute maps onto a td { padding: 0px; } style rule. If the cellpadding setting is removed and not replaced by a style rule (the initial padding value is 0), then some browsers will add 1-2px of padding to the table cells (the default cellpadding value is #IMPLIED), which you may be OK with.

The width="100%" attribute maps onto a table { width: 100%; } style rule - straightforward enough, eh? If the width setting is removed and not replaced by a style rule, then the table will have a shrink-to-fit width and much more closely resemble a standard spreadsheet.

As regards the border="3" attribute, a detailed, down-to-the-pixel discussion of my ultimately unsuccessful attempts to exactly replicate the original chooser borders with CSS would take up more space than it's worth, so let me cut to the chase and alternatively say that I like my borders solid and fully black versus the ridge-like and not fully black borders that we initially get, and therefore I'd go with table { border: solid 3px black; } and td { border: solid 2px black; } style rules for this one.

The table caption

The h3 markup bolds the table caption and increases the caption's font size by a few pixels
and it also puts an empty line box between the caption and the table's first row;
if you feel that the h3 markup is redundant or otherwise inappropriate but like its effects, then you can reproduce those effects with a
caption { font-weight: bold; font-size: 19px; margin-bottom: 18px; } style rule.

Header cells

If we mark up the header cells as th elements versus td elements, then the text in those cells will be bolded and horizontally centered: a th { font-weight: normal; text-align: left; } style rule returns us to the original rendering.

Data cells

We don't want all those align="center"s cluttering up the code, do we? Give the last row's cell an id="resetTd" and replace the align attributes with a td, #resetTd { text-align: center; } style rule. (If you keep the first-column headers in td cells, then you can use a tr > td:first-child { text-align: left; } rule to exclude those cells from the preceding rule.)

Something that didn't work:
The author didn't have access to the colgroup and col elements but we do, and I decided to check if a
<colgroup span="1"></colgroup>
<colgroup span="3" style="text-align:center;"></colgroup>

<colgroup><col span="1">
<col span="3" style="text-align:center;"></colgroup>

snippet would horizontally center the data cell content.
In the event(s), neither snippet had any effect at all, as could have been predicted.

We also have a relatively minor vertical alignment issue to sort out.
The first row's text boxes are preceded by 1., 2., and 3. labels, respectively.
The text boxes in the second and tenth rows are preceded by a $ symbol.
The text boxes in the remaining rows are preceded by an ASCII space** (which is ignored by the browser, per the last ¶ of this HTML 4.01 section) followed by two &nbsp; non-breaking spaces that shift the boxes slightly rightward so that they vertically line up with the $-marked boxes*** or not, depending on the browser's display font - for Times and Times New Roman, Arial, and Helvetica.
To the extent that you want to be a perfectionist about this, there are two fail-safe ways to vertically line up all of the text boxes exactly in the second, third, and fourth columns:
(1) Prepend a &nbsp; to each $ symbol so that each box is preceded by two renderable characters, and then set the table's font-family to a monospace font like Courier.
(2) Delete all of the pre-box characters.

**The ASCII space is not present in the rows[3].cells[1] cell.
***They don't quite line up with the #.-labeled boxes, which are 2-3 pixels to the right.

Input value alignment

Modern browsers will apply the text-align property to relevant input elements although this support is deemed experimental - see the last ¶ of this CSS 2.2 section.
Would you like to horizontally center the Name of Institution values and to right-justify the various cost values?
We can effect the former with a tbody > tr:first-child input { text-align: center; } style rule whereas
an input[type="text"] { text-align: right; } rule will handle the latter.

• As intimated above, the first-in-source-order tr element is the first child of an implicit tbody element; it's not a sibling of the caption element and consequently cannot be selected via a caption + tr next-sibling selector.
• I include an attribute selector in the latter rule so as to exclude the resetting push button in the last row.

The reset button

Per the preceding subsection, an input[type="button"] { width: 150px; } styling can be applied to the reset button if you do in fact want it to have a width of 150px for whatever reason.
We'll take on the chooser's JavaScript in the following entry.

Powered by Blogger

Actually, reptile7's JavaScript blog is powered by Café La Llave. ;-)