Form of...HTML!

jasonharper

Developer
htmlform.ash (currently version 0.2) is a code library for building relay browser scripts that accept user input.

If you're a script writer wanting to use it, see the second message in this thread for the documentation, and the third for a simple tutorial.

If you were directed to this thread because a script you want to use requires htmlform.ash, follow these steps:
* Make sure you have a sufficiently recent build of KoLmafia - r8338 or later is needed. The 14.0 release version is NOT new enough.
* Download the attached file, and place it in mafia's scripts folder - but note that the scripts that make use of htmlform will go in the relay folder, instead.
* Look for a -run script- menu in the bottom right corner of the top menu pane, in the relay browser. If you downloaded the scripts while mafia was already running, click the [re] button in that corner to refresh the menu.

Revision history:
0.2: Fixed a couple of problems with write_check().
0.1: Initial release.
 

Attachments

  • htmlform.ash
    18.4 KB · Views: 4,213
Last edited:

jasonharper

Developer
[highlight]SCOPE[/highlight]
The purpose of htmlform.ash is to allow you to write relay scripts without necessarily having a deep knowledge of HTML forms (although you will need to know at least a bit about HTML formatting if you want your scripts to look nice). It is designed to make the process of gathering input from the user look like a simple sequence of prompts, when in fact the script is being reloaded multiple times in order to present and validate the user's choices.

[highlight]GLOBALS[/highlight]
There are two global variables defined by the library that you'll need to know about:
string[string] fields is a map holding the result of a call to form_fields(). The current value of any input element can be retrieved as fields[name], although you probably won't have to do that very often as the functions that actually write the elements to the page will return the current value of that element. You can also modify values in the map, prior to writing the corresponding element, to change their value - for example, to change the default values of certain fields based on the user's input in other fields.
boolean success is true when a valid page submission has occurred - the fields map will be non-empty (unlike the initial run of the script in which the choices are initially shown in the browser), and none of the fields have a validation failure. Button actions should generally be conditional on this variable being true (and will automatically be so, if you use the test_button() function).

All other internal global variables, and functions for internal use, have names starting with an underscore, to minimize the chance of a name collision with your script.

[highlight]NAMES[/highlight]
Every HTML form element requires a distinguishing name, which for maximum compatibility should be chosen with the same basic rules as ASH variables - start with a letter, use only numbers and letters. Take care if you're generating names automatically; for example, if you were generating a field for each item in a list, it would be a bad idea to use the actual item names as (part of) the fields' names, since items may have arbitrarily weird characters in their names. The items' ID numbers (retrievable with to_int()) would be a much safer basis for making field names.

For the most part, element names must be distinct (and strange things will happen if you violate that). There are two exceptions:
* Radio buttons normally come in mutually-exclusive groups, all of which must share the same name - in fact, that's the only thing that relates them as a group (there's no requirement for them to be physically grouped, although of course that would usually be good style).
* Submit buttons can share a name if they do the same thing - for example, you might want to have a "Save changes" button at both the top and bottom of a tall form. If a button has no action at all (it just updates the form), its name can be left blank.

[highlight]ATTRIBUTES[/highlight]
There is a large and ever-growing set of optional attributes that can be specified to customize the appearance and behavior of HTML form elements. It would be utterly impractical for htmlform to have optional parameters for every possible attribute, or even for every particularly useful attribute. Instead, you can supply arbitrary attributes by preceding the call that writes an element with one or more calls to:
void attr( string attribute )
The string will be inserted at the appropriate place in the HTML tag being written. If called multiple times, the strings will be separated by spaces.

USEFUL ATTRIBUTES
attr("title='TEXT'") will add a tooltip to any element.
Advanced users may want to use style & class attributes for fine control over element appearance.
A complete list of attributes is entirely beyond the scope of this documentation (see any HTML reference, instead), but the most useful ones will be pointed out in blocks like this.

[highlight]VALIDATORS[/highlight]
The functions that produce a field for the user to type in (write_field() and write_textarea()) can be passed the name of a validator function that can check (and possibly modify) the field value. Any validation failure is shown in red to the right of the field, and blocks submission of the form until the user corrects the problem. Validator functions are provide for all the datatypes for which typed-in values make sense:
intvalidator, floatvalidator, itemvalidator, locationvalidator, skillvalidator, effectvalidator, familiarvalidator, monstervalidator
(They AREN'T provided for types like 'class' and 'slot' that have only a few possible values; other input elements are more appropriate in those cases).
None of these accept the 'none' value; there is a separate itemnonevalidator that will accept items or 'none' (similar validators for other types didn't seem likely to be as useful). There is also a nonemptyvalidator that simply requires the entered string to not be blank.

intvalidator and floatvalidator additionally support bounds checking via the function:
void range( float min, float max ) - place this just before the call to write_field().

The provided validators only check that the entered text is something understandable to KoLmafia - not that you (for example) actually have the named familiar, or that the item goes in some specific slot. If you want to perform such checks, you'll need to write your own validator, which is just a function with one string parameter (the element name), that returns a string. Any non-empty string is treated as a validation error. Returning an empty string indicates success, in which case the function is allowed to modify the value in the fields[] map, which will update the value shown in the browser. (That's why the function is passed the field name, rather than the field value. All of the built-in validators that allow fuzzy matching use this feature, in order to replace an abbreviated user entry with the full, proper name of the object.) Here's a simple example that verifies that the entered text is a valid item (duplicating the function of itemvalidator), which furthermore is a hat that you own:
PHP:
string hatvalidator(string name)
{
    item it = to_item(fields[name]);
    if (it == $item[none]) {
        return "A valid hat is required.";
    }
    fields[name] = to_string(it);    // normalize
    if (item_type(it) != "hat") {
        return "That's not a hat.";
    }
    if (available_amount(it) == 0) {
        return "You don't have that hat.";
    }
    return "";    // success!
}
You'd then pass "hatvalidator" as the 4th parameter to write_field().

[highlight]TOP LEVEL FUNCTIONS[/highlight]
void write_header()
void finish_header()
void write_page() - equivalent to write_header() followed by finish_header()
void finish_page()
All scripts using htmlform must use these functions to generate the overall page structure. You can use write_page() at the top of your main() function and finish_page() at the bottom; you can also split the write_page() into separate write_header() and finish_header() calls if you need to write something into the page header yourself (such as a script or stylesheet). All of the other htmlform function calls MUST be located between write_page/finish_header and finish_page (or in functions that you call from within that range, of course).

void write_box( string label )
void finish_box()
These can be used (in matched pairs only, please!) anywhere within the body of the page, to produce a box around a group of related elements. The label will typically appear at the top left of the box; it can contain HTML markup, or can be blank. (Internal details: this produces a <fieldset> tag, possibly containing a <legend>.)
USEFUL ATTRIBUTES
attr("align=center") or attr("align=right") will change the position of the label.

The following two functions are actualy part of ASH, but I'll mention them here because you'll be using them a lot:
void write(string)
void writeln(string)
These are only valid in a relay script, and produce raw output to the relay browser. The string will generally be interpreted as HTML; if it may contain characters that have special meaning in HTML (such as ampersands and angle brackets), you can use entity_encode() on the string to prevent any such interpretation.
The only difference between the two functions is that writeln() appends a line break to the string; line breaks are normally not significant in HTML, so they will generally have the same effect. In particular, writeln() will not normally produce a visible line break in the browser; you'll need to use a HTML tag like <br> for that. The main reason for using writeln() is to make the HTML more human-readable, which may be important if you aren't getting the results you expect, and need to 'View Source' in the browser.

[highlight]GENERAL USAGE[/highlight]
The functions that actually produce form elements in the browser will usually have a first parameter that specifies an initial value (of some specific datatype), and will return an updated value (of the same type). On the initial run of the script (when the user hasn't had a chance to enter anything yet), and whenever the field value fails validation, the initial value will be returned, otherwise the user's entry. The general idiom for writing elements depends on what you'll be doing with these values:

Stored in a local variable (no persistence):
float favoriteNumber = write_field( 42.0, ...
Note that the use of a floating-point number for the initial value is significant; using the integer 42 instead would result in a field that only accepted integers.

Saved in a KoLmafia preference:
set_property( "favNum", write_field( to_float(get_property("favNum")), ...
Note that type conversion will generally be required, as properties are always treated as strings. You could also use a string field, and explicitly choose the appropriate validator:
set_property( "favNum", write_field( get_property("favNum"), ..., "floatvalidator"));

Saved in a Zlib script setting:
vars["favNum"] = write_field( to_float(vars["favNum"]), ...
You'll need to explicitly write the vars back to disk on success; sorry, I don't know the usual idiom for that.

[highlight]BASIC ELEMENTS[/highlight]
Single-line text entry fields (HTML <input type=text>):
<type> write_field( <type> initialValue, string name, string label )
<type> write_field( <type> initialValue, string name, string label, string validator )

Supported <type>s are string, int, float, item, location, skill, effect, familiar, monster. The 3-parameter version uses the default validator for <type> (except for strings, which default to no validation).
'name' is the element name as described previously, and must be unique.
'label' is text (possibly containing HTML markup) to be placed to the left of the field. It can be left blank if you want no label, or will be providing a label separately via write_label().
USEFUL ATTRIBUTES
attr("type=password") - obscures the text entered in the field.
attr("type=search") - may give the field OS-dependent features suitable for a search field, such as a distinct appearance and a button to clear the field. This is not widely supported yet, but is supposedly backwards compatible.
attr("size=N") - resizes the field so that about N characters will fit without scrolling.
attr("maxlength=N") - prevents entry of more than N characters in total.
attr("placeholder='TEXT'") - shows grayed-out text when the field is empty and unselected, as a hint to what you should type there. Only applicable to string fields, since no other type ever has an empty value. Ignored in older browsers.

Multiple-line text entry (HTML <textarea>):
string write_textarea( string initialValue, string name, string label, int cols, int rows)
string write_textarea( string initialValue, string name, string label, int cols, int rows, string validator )

Like the above, but supports multiple lines of text, with scrolling. The number of columns and rows are required; there is no validator by default. This is only provided in a string version, since no other datatype can meaningfully have a multi-line value.
USEFUL ATTRIBUTES
attr("readonly") - the text can still be scrolled and selected, but can't be edited. Useful for text that is intended to be copied & pasted elsewhere.

Either of the above (as well as some other functions described laer) can have a separate label (HTML <label for>) that isn't directly attached to the field; this is useful when using tables to lay out the page nicely, and the labels need to go in a different column than the fields.
void write_label( string name, string label )
The label can contain HTML markup.
The name must match the name used in a write_field(), write_textarea(), write_select(), or write_choice(), that itself does not specify a label. The label can appear before or after the field on the page.
This isn't supported for checkboxes, which generally wouldn't benefit from having a detached label.
It's also unsupported for radio buttons, which have the additional problem of needing more than just the name to identify a particular button (since all the buttons in a group must share the same name).

string write_hidden( string initialValue, string name )
This is effectively a field that isn't shown to, or editable by, the user - it's just passed on from page load to page load. This might be useful if your script relies on some value that requires lengthy calculation; you can avoid recalculating it each time. Only a string version is provided, use the ASH type conversion functions if you want to remember a value of a different type.

boolean write_check( boolean initialValue, string name, string label )
Produces a checkbox element; not much to say about this one.

<type> write_radio( <type> initialValue, string name, string label, <type> value )
void write_radio( string label, <type> value )

Produces a radio button; <type>s supported are all the basic ASH datatypes except boolean. The 2-parameter form reuses the initialValue and name parameters from the previous 4-parameter call; those two are required to be the same for every radio button in a group (although you could use the 4-parameter version every time, if that's more convenient).
Each individual radio button will appear marked when the current value (from initialValue or user selection) matches its 'value' parameter. The results aren't well-defined if two buttons in a group have the same value, or if none of the values match the initialValue - don't do that.
It is quite likely that you'll provide the same value for both 'label' and 'value'.

[highlight]SELECT & CHOICE ELEMENTS[/highlight]
These functions produce a popup menu with an arbitrary set of choices - basically a more compact version of a radio button group. The required structure is that every call to write_select() must be balanced by a call to finish_select(); in between, the only valid calls are write_option()s, and maybe some balanced (but non-overlapping) calls to write_group() and finish_group().
<type> write_select( <type> initialValue, string name, string label )
Supported <type>s are all basic ASH types except boolean.
USEFUL ATTRIBUTES
attr("onchange='document.relayform.submit()'") - selecting a value from this popup will automatically submit and validate the form, just as if a submit button were clicked.
attr("size=N") - presents the choices as a scrolling list that's N lines tall, instead of a popup.
attr("multiple") - allows selection of more than one choice at a time. htmlform doesn't fully support this yet, and the return value from write_select() is no longer useful. Ask if you want details on how to use this, but please keep in mind that multiple selection lists aren't the most intuitive UI out there - many users won't known how to select multiple items from them.

void finish_select()
Don't forget to finish what you started!

void write_option( string label, <type> value )
void write_option( <type> labelAndValue )

Adds a choice to the list. The 1-parameter version uses the string version of the value as the label, which is quite often what you want.

void write_group( string label )
void finish_group()

Identifies the contained group of write_option() calls as being related, with the given label describing the group. The exact appearance depends on the browser; some will use indentation to indicate the grouping, others will create a submenu for each group. Old browsers will ignore groups completely.

For your convenience, a function is provided that wraps calls to write_select() and finish_select(), with a loop of write_option() calls in between. The downside is that you have no control over the choice labels, they're always. the same as the values.
<type> write_choice( <type> initialValue, string name, string label, boolean[<type>] values )
A choice is added for each key in the values map that has a true value; note that this is exactly the datatype returned by plural typed constants, so you can easily specify a constant list of values, or use a form like $items[] to get a list of every known item in the game.
Possible attributes are the same as for write_select().

item write_choice( item initialValue, string name, string label, int[item] values )
This variant only works with one fairly common data structure, as returned by get_inventory() and several other ASH functions. The choice labels will include the integer value from the map, in parentheses after the item name.

[highlight]BUTTONS[/highlight]
In order to actually receive any input from the user, the form in the relay browser has to be submitted back to KoLmafia. It's possible to attach JavaScript to the form elements to automatically submit the page when their value changes, but a more common approach would be to provide explicit buttons for this purpose.
boolean write_button( string name, string label )
'name' can be left blank if you never need to know that this particular button was clicked, in which case the function always returns false. Otherwise, it returns the same value as test_button(name):

boolean test_button( string name )
Returns true if the named button was responsible for submitting the form, AND the global 'success' variable is true (indicating that no field validation errors were found). This is provided as a separate function as the point at which the test is done is significant. If the button's action depends on all the other fields (as is normally the case), the test has to be done at the very end of the page processing - after all the fields have been processed and validated - even if the button is physically located at the top of the form. On the other hand, a button that resets fields to some default value (by changing their values in fields[]) would need to be tested near the start, before the fields themselves are processed - even if the button itself is written at the end of the form.

Note that fields are only validated on a button click, normally. If your script performs any immediate action (rather than just saving values for later use), and there are any validated fields involved, it might be best to provide two buttons - one to check the form (it would have no action attached, and would need no name), and another to actually perform the task, once the user has seen that all their typed-in values were interpreted as desired.

Attached is a test script that uses just about every type-specific version of every htmlform function. The current value of each element is simply displayed to the element's right, so this isn't a very good example of how you'd actually use the functions, but is more of a showcase of what you can do with them.
 

Attachments

  • relay_test.ash
    6 KB · Views: 173
Last edited:

jasonharper

Developer
Okay, here's a quick little walk-thru for putting all the pieces together - in the form of a silly little script that multiplies two numbers. (I intentionally avoided anything related to actual items or adventuring, to make it clear that there are no possible side-effects from playing with it.)

Let's get started with some boilerplate:
PHP:
import "htmlform.ash";
void main()
{
	write_page();
	
	writeln("To be continued...");
	
	finish_page();
}
That's the basic structure for any script using this library; the only variation available is to replace write_page() with write_header() and finish_header(), allowing you to do advanced, headerly-type things in between the two. Save this in the relay folder, with a name like "relay_multiply.ash" (both the "relay_" prefix and ".ash" suffix are required), and you've got a working script - albeit an amazingly useless one.

To make this script actually do its intended job, we need a couple of fields for entering the numbers, and a button to multiply them. Next version:
PHP:
import "htmlform.ash";
void main()
{
	write_page();
	
	writeln("This script multiplies numbers - so you don't have to!");
	
	float a = write_field(0.0, "input1", "First number:");
	float b = write_field(0.0, "input2", "Second number:");
	if (write_button("calc", "Calculate")) {
		writeln("The product is " + (a * b));
	}
	
	finish_page();
}
Some notes here:
* The two fields and the button are given distinct names - input1, input2, calc.
* The initial values for the fields are given as the floating-point number 0.0, so the fields automatically accept floating-point values. If I'd used the integer 0, they'd have been integer-only fields; if I'd used something like $item[none] they'd accept KoL item names, and so on.

Running the script at this point will produce something like this:
Picture 3.png
Ok, that's just plain ugly - but it is fully functional at this point, including a couple of non-obvious features:
* Entering invalid data in either field will produce a red error message next to it, and no result will appear.
* Clicking on the field labels selects the corresponding field for editing.

To make the script look nicer, we're going to have to deal with some actual HTML tags - options for basic formatting include <br> for a linebreak, <p> for a linebreak with more space, and <hr> for a horizontal dividing line. (htmlform doesn't include any specific functions for writing these, since it would be just as easy for you to remember the tag itself as it would be to remember a function name that produces it.) Also, those fields are much wider than they need to be to hold any reasonable number we might want to multiply, so let's use a HTML size attribute to make them shorter. Now we have:
PHP:
import "htmlform.ash";
void main()
{
	write_page();
	
	writeln("This script multiplies numbers - so you don't have to!");
	writeln("<br>");
	attr("size=10");
	float a = write_field(0.0, "input1", "First number:");
	writeln("<br>");
	attr("size=10");
	float b = write_field(0.0, "input2", "Second number:");
	writeln("<br>");
	if (write_button("calc", "Calculate")) {
		writeln("The product is " + (a * b));
	}
	
	finish_page();
}

Which produces output like this (with an error state currently being shown):
Picture 5.png
Aha, that's much better - the main remaining aesthetic complaint is that the fields don't line up with each other, due to the different sizes of their labels. Fixing that requires delving further into HTML: one possibility would be to use tables to put everything on a regular grid. Quick summary: a table is formed by <table>...</table>. A table contains rows: <tr>...</tr>. Rows contain cells: <td>...</td>. And cells are where you put your content. The code becomes:
PHP:
import "htmlform.ash";
void main()
{
	write_page();
	
	writeln("This script multiplies numbers - so you don't have to!");
	writeln("<br>");
	writeln("<table><tr><td align=right>");
	write_label("input1", "First number:");
	writeln("</td><td>");
	attr("size=10");
	float a = write_field(0.0, "input1", "");
	writeln("</td></tr><tr><td>");
	write_label("input2", "Second number:");
	writeln("</td><td>");
	attr("size=10");
	float b = write_field(0.0, "input2", "");
	writeln("</td></tr></table><br>");
	if (write_button("calc", "Calculate")) {
		writeln("The product is " + (a * b));
	}
	
	finish_page();
}
Note that the fields no longer specify a label themselves; the labels (now in a different cell of the table than their field) are produced by write_label(), and associated with a field by the use of the same name. (I could have simply used write("First number:"), write("Second number:") to produce the labels, and that would look exactly the same, but that would have lost the feature of being able to click on a label to select the field. Proper use of labels is also important for people who use a screen reader or other non-visual browsing method, although I'm not clear on the details.) Here's what the table version looks like:
Picture 6.png
I'm going to call that good enough for all practical purposes.
 
Last edited:

StDoodle

Minion
O M F G. I am nearly speechless, and can't wait to put this into action. For reference, are the functions in your script intended to make it into the core of KoLmafia once they've been tested?

Edit to add:
The following line doesn't make sense to me.
Code:
err = call string validator(name);
Is there functionality that is undocumented (to my knowledge) for selecting which function to call based on a variable or something? What am I missing?

Edit 2:
Ooh, it appears there is! The variable must represent the full function name, but otherwise.... that will be so SWEET when refactoring customdaily.ash!
 
Last edited:

jasonharper

Developer
The ability to call a function whose name is given by a variable has been around for a while...

Nothing in htmlform would actually benefit from being a built-in function; it's all just elaborate, indirect ways of calling write(). I can imagine shipping the file with KoLmafia someday, if it proves to be widely useful, but I'd consider it wasted effort to ever convert it all to built-in functions.
 

StDoodle

Minion
The ability to call a function whose name is given by a variable has been around for a while...

I'm still fairly new to ASH. ;) This is exactly the kind of thing I need to put up on the wiki.

Nothing in htmlform would actually benefit from being a built-in function; it's all just elaborate, indirect ways of calling write(). I can imagine shipping the file with KoLmafia someday, if it proves to be widely useful, but I'd consider it wasted effort to ever convert it all to built-in functions.

I'm mostly approaching this from the non-scripter's perspective. There are lots of scripts out there (mine included) that could massively benefit from this kind of improved RB interaction. Sure, it won't be a huge deal for most of the regulars on this forum; but for the "average joe" user, this will make a lot of maifa scripts much more accessible. I think there a quite a few people out there who are a bit overwhelmed by setting variables for a script on the command line or in the script itself. Your library will go a long way toward overcoming that barrier, if used well.
 

Bale

Minion
Hurm... Is this a bug?

Code:
string write_check(boolean ov, string name, string label)
{
	if (label != "" ) {
		write("<label>");
		write(label);
	}
	if (fields contains name) {
		ov = true;
	} else if (count(fields) > 0) {
		// Unfortunately, there's no way to detect the presence
		// of a checkbox that's not currently checked - assume
		// it's actually there if there are any submitted fields.
		ov = false;
	}
	write("<input type=\"checkbox\" name=\"");
	write(name);
	write("\"");
	if (ov) {
		write(" checked");
	}
	_writeattr();	
	if (label != "" ) {
		writeln("</label>");
	}
	return ov;
}
Shouldn't that function return a boolean? Or do I misunderstand?
 
Last edited:

Bale

Minion
I found a use for write_hidden().The purpose is for the update button to give an indication that it has done something, even after the first click. Is really the best way to accomplish the effect?

Code:
	if(fields contains "updcol")
		switch(fields["updcol"]) {
		case "blue":
			fields["updcol"] = "green";
			break;
		case "green":
			fields["updcol"] = "Darkorange";
			break;
		case "Darkorange":
			fields["updcol"] = "magenta";
			break;
		case "magenta":
			fields["updcol"] = "blue";
		}
	if(write_button("upd", "Update"))
		write("<a style=\"color:"+write_hidden("blue", "updcol")+"\"> Updated!</a>");
 

jasonharper

Developer
Interesting idea - the only thing I'd suggest doing differently is to use a <span> tag instead of <a> to hold the colorized text. (An <a> with no HREF or NAME attribute is weird, although technically legal.)
 

heeheehee

Developer
Staff member
<a> tags are also faster to type, incidentally. For some odd reason, I don't think that's what Bale was going for...

Edit: But hey, new idea for screwing with total HTML newbs when throwing together sites!
 

Bale

Minion
Thanks for the span information -- I'll fix that. I'm just picking up html as I go along.

I guess my update color changing method works properly after all?

By the way, was it intentional that the only way to access or change information written with write_hidden() is to check and modify the value in fields?
 

StDoodle

Minion
Ok, busy couple of weeks, so I could have totally misread things here... but does Bale's code sample write a hidden field AS PART OF ANOTHER TAG'S ATTRIBUTE VALUE? 'Cause no, that's not good form...

Please ignore me if I'm just crazy.
 

Bale

Minion
Code:
write("<span style=\"color:"+write_hidden("blue", "updcol")+"\"> Updated!</span>")
Yes... Yes I think I do. Is that naughty? All I know is that it worked when I tried it.
 

StDoodle

Minion
Hmm, depends on how write() declarations work... if that ends up processing the <input> for the hidden field first, then going back and doing the write() in your code snippet using the return value of write_hidden(), then you're okay. If instead it starts the write() in your code, and then writes the entire <input> of hidden type inside of it, then that would be BAD FORM. I'd have to test or see the output to know...
 

jasonharper

Developer
The parameters to a function call are entirely executed before the function itself; the write_hidden() call therefore generates the hidden field prior to the outer write() call doing anything, so there's no problem here.
 

icon315

Member
Hey i was trying to figure out if there was a way of hidding a certain text until a picture/text is clicked. Sort of what the Mr Script does with the Top pane. I tried some things but they didn't work at all. can you guys help?
 

heeheehee

Developer
Staff member
Odds are you'd want to do some sneaky stuff with JavaScript -- heck, if you wanted, you could just yoink the relevant parts of Mr. Script and apply that to the topmenu relay override. Of course, that requires going through that massive script... CTRL-F is your friend?
 

icon315

Member
i already found the part in Mr Script

Code:
 toprow2.appendChild(document.createElement('br'));
		AddTopLink(toprow2, 'mainpane', 'doc.php?topic=home', 'documentation', 1);
		AddTopLink(toprow2, 'mainpane', 'adminmail.php', 'report bug', 1);
		AddTopLink(toprow2, '_blank', 'http://store.asymmetric.net', 'store', 1);
		AddTopLink(toprow2, '_blank', 'donatepopup.php', 'donate', 1);
		AddTopLink(toprow2, '_blank', 'http://forums.kingdomofloathing.com', 'forums', 1);
		AddTopLink(toprow2, '_blank', 'radio.php', 'radio', 1);
		a = document.createElement('a'); a.innerHTML = "more"; a.setAttribute('href','#');
		a.addEventListener('click', function(event)
		{	var tr2 = document.getElementsByName("toprow2")[0];
			var tr1 = document.getElementsByName("toprow1")[0];
			tr2.style.display = "none"; tr1.style.display = "inline";
			SetPref('toprow', 1);
		}, true); toprow2.appendChild(a);
I just can't seem to figure out how to manipulate it to work for me
 
Top