Nov 14 2008

Extending Prototypes

Category: JavaScriptJonathan Fingland @ 10:14 am

JavaScript is described as a prototypal language because of it’s use of a shared prototype/cookie cutter for objects. It’s probably easier to understand with some examples, though.

function User (name) {
    this.name = name;
}

var jon = new User("Jon");
alert(jon.name);

The above will show an alert dialog with the name of the user, “Jon”. All instances of User will have a name property, but we can can do more:

function User (name) {
    this.name = name;
    this.say_name = function() {
        alert(this.name);
    }
}

var jon = new User("Jon");
jon.say_name();

Now all users will have a method, say_name. Unfortunately, every User instance has a new method called say_name. It's not a complex function, but there's potentially a lot of needless duplication. If say_name doesn't need to know private details, we can do the following instead:

function User (name) {
    this.name = name;
}

User.prototype.say_name = function() {
    alert(this.name);
}

var jon = new User("Jon");
jon.say_name();

Now all User instances share the same say_name function.

A couple of days ago, I posted about First-class functions in JavaScript and in particular the map function. As written, it works fine, but how could we take advantage of the prototype property shown above. I mentioned that JavaScript 1.6 adds support for a map function, but older browsers don't support it so we can do this instead:

if (typeof Array.prototype.map == "undefined") //check if it isn't already defined (e.g. IE 6)
{
    Array.prototype.map = function (callback) {
        var ret_array = [];
        for(var index = 0, length = this.length; index < length; ++index) {
            ret_array[index] = callback(this[index]);
        }
        return ret_array;
    }
}

Now there is one small problem. The map function defined in JavaScript 1.6 takes an optional 2nd parameter for the context of this. We can fix the above by changing the method as follows:

    Array.prototype.map = function (callback, context) {
        var ret_array = [];
        for(var index = 0, length = this.length; index < length; ++index) {
            ret_array[index] = callback.call(context, this[index]);
        }
        return ret_array;
    }

I've used the call method which belongs to all Function objects. See call at MDC for more detail on that. It allows you to change the context of the execution of a function... letting 'this' refer to something different. I'll write more about that later as it really deserves its own post.

So, now we have a standard compliant, backwards-compatible (back to JavaScript 1.3 compliant browsers) map function. If the browser supports it, the native map function will be used, and if not, our substitute will be used instead. Feel the Win.

There's a lot more you can do with this, but that should be enough to get started.


Nov 12 2008

First-class functions in JavaScript

Category: JavaScriptJonathan Fingland @ 1:31 am

In JavaScript functions are first class objects. JavaScript isn’t alone in this by any means, but it’s a very powerful feature that a lot of beginners are unaware of.

To start off, think of a variable declaration:

var room_number = 1406;

and you can pass that into any function, like so:

alert(room_number);

Well you can do the same thing with functions. Basically there are two common ways to create functions, and another less common way. Everybody reading this is probably familiar with this form:

function say_name(name) {
    alert('Your name is ' + name);
}

Obviously we’ve created a function called ’say_name’ and if I want to use it, simply call say_name("Jon"). However we can accomplish the same thing with the following:

var say_name = function(name) {
    alert('Your name is ' + name);
}

In this case we created an anonymous function and then gave it a name. Another nice feature here is that we can do the same thing when adding an event handler — just pass the anonymous function. Look at the two snippets below:

function announce_click(event) {
    alert("you clicked!");
}

var myButton = document.getElementById("myButton");
myButton.addEventListener("click",announce_click,false);

Passing the function name to addEventListener, and…

var myButton = document.getElementById("myButton");
myButton.addEventListener("click",
                               function (event) {
                                     alert("you clicked!");
                               },
                               false);

…passing an anonymous function to do the same thing.

A word of caution though. When using removeEventListener, you must pass it the same function. Another anonymous function that does the same thing is not good enough. So if you never need to remove an event handler then declaring the function in the handler works great. Otherwise, it’s often a good idea to give the function a name so you can refer to it later.

Well, that’s all fine and dandy, but what else can you do with it? Well, it’s a natural fit for a callback. For those not familiar with the term callback, it’s simply a way of telling the function you’re calling to call another function as well. In the examples above, addEventListener took a callback in the 2nd parameter.

If you’re familiar with PHP’s callback system of passing a string with the name of the callback, rest easy. This is much better. JavaScript 1.6 has a map function on Arrays, but since backwards compatibility is often critical, I could use something like the following:

function map(callback,array) {
    var ret_array = [];
    for(var index = 0, length = array.length; index < length; ++index) {
        ret_array[index] = callback(array[index]);
    }
    return ret_array;
}

The map function will loop through the elements of the array and, for each one, call the function callback and pass it the value of the element. Then it assigns the value returned by callback to the matching index in a new array. Finally, it returns the new array.

The important thing here is that callback could be just about anything. Consider this:

function square(number) {
    return number * number;
}

var my_array = [1,2,3,4];

var squared_array = map(square, my_array);
//squared_array contains [1,4,9,16]

As you can see, using the map function we were able to easily apply the same function to every element of the array and get an array of the return values. Obviously the possible applications are pretty much endless. Have fun with it. Just remember that functions are objects too -- which brings me to the third, much less common way of creating functions. I can't say as I've ever seen this in the wild, and I don't recommend using it, but for completeness here it is:

var say_name = new Function("name", "alert('Your name is ' + name);");


Oct 26 2008

Passing parameters into a created dialog, and retrieving them on exit

Category: DOM, Firefox, JavaScript, Pirate Questing, XULJonathan Fingland @ 7:50 am

This is going to be a pretty short tutorial today to help explain how to pass information to an window when opening it. The following is taken from PirateQuesting.

First, here is the function for entering the code when PirateQuest asks the user for verification.

function enterCode(url,imgsrc, func) {
	var params = {in:imgsrc, out:null};
	window.openDialog("chrome://piratequesting/content/codeDialog.xul", "",
		"chrome, dialog, modal, resizable=no, status=no,
		height=250, width=400", params).focus();
	if (params.out) {
		piratequesting.Code.submit(url,params.out,imgsrc,func);
	}
	else {
	    // User clicked cancel. stop here
	}
}

So, as we can see in the first line the params variable stores a hash. A has is useful here as it allows us to easily pass more than one variable in without goign to the work of creating an object. There are actually much more significant differences between a hash and an object but, for this tutorial, know that it stores values in name:value pairs separated by commas and all of it enclosed by curly braces. The last element must not be followed by a comma.

Next, when we use openDialog we pass params into the dialog.

After the user has clicked OK, the value of params.out is checked. The condition will be true unless the value is still null or by some strange miracle taken on a value like ‘false’.

Now, let’s look at the code behind the dialog itself

function codeDialogOnLoad() {

    // Use the arguments passed to us by the caller
    document.getElementById("codeImage").setAttribute('src',
            window.arguments[0].in);
}

// Called once if and only if the user clicks OK
function onOK() {
    window.arguments[0].out = document.getElementById("codeValue").value;
    return true;
}

Ok, so what do we have here? well, when the dialog first loads we call codeDialogOnLoad which then sets the image source on the dialog based on the value passed in params.in. Note that it is now referred to as window.arguments[0].in.

When the user presses OK, the value of an input box, codeValue, is assigned to params.out (a.k.a. window.arguments[0].out).

Last thing to look at is the codeDialog.xul

<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
<dialog
  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
  id="codeDialog"
  title="Enter the Code"
  ondialogaccept="return onOK();"
  buttonlabelaccept="Submit"
  onload="codeDialogOnLoad();"
  persist="screenX screenY width height">

	<script type="application/x-javascript" src="chrome://piratequesting/content/codeDialog.js"/>
	<vbox>
		<label value="Enter the code shown below" />
		<image id="codeImage" />
		<textbox width="50" id="codeValue" />
	</vbox>
</dialog>

As you can see, codeDialog.xul is very simple and contains only three elements inside a vbox. This is really one of the simplest examples you could use and was chosen to illustrate how to simply and easily pass information into and out of a dialog

Tags: , , , ,


Aug 29 2008

Javascript controls on filmstrip

Category: CSS, DOM, JavaScriptJonathan Fingland @ 12:35 pm

In this article we’re going to look at how to add some direction controls to our filmstrip. At the end of the last article we had the following filmstrip:

Horizontal scrollbars in <div> tags don’t really play that nicely with scroll wheels or key controls unless you set the tabindex property. I’ve done so on the filmstrip above, and given it tabindex="1" so it’s the first thing you tab to. Just press left or right to test it out. And, although the scrollbars work well enough, let’s try something different. There are several ways to accomplish this, but some are distinctly better than others. I say better because of how well, or poorly, they play with browsers.

The method I describe below is doing nothing more than changing the display css-attribute. It also uses a couple of different ways to move the filmstrip images.

First let’s look at the events. Using the following for your onload function:

function onLoad() {
	if (document.addEventListener) {
		document.getElementById('strip').addEventListener('keypress',HandleKeyPress,false);
		document.getElementById('strip').addEventListener('DOMMouseScroll',HandleWheel,false);
		document.getElementById('larrow').addEventListener('click',moveLeft,false);
		document.getElementById('rarrow').addEventListener('click',moveRight,false);
	} else {
		document.getElementById('strip').onkeypress = HandleKeyPress;
		document.getElementById('strip').onmousewheel = HandleWheel;
		document.getElementById('larrow').onclick = moveLeft;
		document.getElementById('rarrow').onclick = moveRight;
	}
	thumbNum = document.getElementById("imagerow").getElementsByTagName("li").length;
	setLeft();
}

The only reason for the difference is that some browsers don’t support the addEventListener method. setLeft() we’ll see a little bit later on. thumbNum is just a convenient store for the number of thumbnails.

Next let’s look at some of the variables we’re using and how they’re initialized:

var thumbNum;    //total number of thumbnails
var showNum = 4; //how many thumbnails to show
var curLeft = 0; //current index of the leftmost thumbnail

thumbNum we already know gets initialized in the onLoad function. showNum and curLeft are used throughout the script.

Next, let’s look at the events and what they call:

function HandleKeyPress(e) {
	switch (e.keyCode) {
		case e.DOM_VK_LEFT:
			moveLeft();
			break;
		case e.DOM_VK_RIGHT:
			moveRight();
			break;
		case e.DOM_VK_ESCAPE:
			content.focus();
			return;
	}
}

/** Event handler for mouse wheel event.
 * originally from http://adomas.org/javascript-mouse-wheel/
 */
 function handle(delta) {
        if (delta < 0)
		moveRight();
        else
		moveLeft();
}

function HandleWheel(event){
        var delta = 0;
        if (!event) /* For IE. */
                event = window.event;
        if (event.wheelDelta) { /* IE/Opera. */
                delta = event.wheelDelta/120;
                /** In Opera 9, delta differs in sign as compared to IE.
                 */
                if (window.opera)
                        delta = -delta;
        } else if (event.detail) { /** Mozilla case. */
                /** In Mozilla, sign of delta is different than in IE.
                 * Also, delta is multiple of 3.
                 */
                delta = -event.detail/3;
        }
        /** If delta is nonzero, handle it.
         * Basically, delta is now positive if wheel was scrolled up,
         * and negative, if wheel was scrolled down.
         */
        if (delta)
                handle(delta);
        /** Prevent default actions caused by mouse wheel.
         * That might be ugly, but we handle scrolls somehow
         * anyway, so don't bother here..
         */
        if (event.preventDefault)
                event.preventDefault();
	event.returnValue = false;
}

Looking at HandleKeyPress we see that it does a short switch-case on the keycode from the event. You can use this same technique to add other keypress events. There are also keyup and keydown events which have their uses.  The best list of the keycodes available is here.

The mouse wheel event I’ve borrowed from another site (no point in re-inventing the wheel). The annoying part about the mouse wheel is that every browser interprets the event differently. Sooo… the script has to check for them all.

Both, the mouse wheel event and the keypress event, call moveLeft() and moveRight(), which are shown below:

function moveLeft(){
  if (curLeft == 0) return; //already at the left
  else {
    curLeft = curLeft - 1;
    setLeft();
  }
}

function moveRight() {
  if (curLeft==thumbNum-showNum) return; //already at the right
  else {
    curLeft = curLeft + 1;
    setLeft();
  }
}

All these do is check to see if they’re already at the left or the right and then increase or decrease curLeft and call setLeft(). setLeft is what does our styling changes.

function setLeft(){
	var rng = getRanges();
	for (var i = 0; i < rng.out.length; i++ ) {
		rng.out[i].setAttribute("style","display:none;");
		rng.out[i].style.display = "none";
	}
	for (var i = 0; i< rng.in.length; i++ ) {
		rng.in[i].setAttribute("style","display:inline;");
		rng.in[i].style.display = "inline";
	}
}

The first thing it does is get the ranges of elements which will be hidden or shown. Then it loops through those outside the range to be shown and hides them. Finally it loops through those inside the range to be shown and displays them.

function getRanges() {
	var end = curLeft + showNum - 1; //calculate the image position at the end of the display

	var iva = new Array();	//create arrays for the in and out of range elements
	var ova = new Array();

	//get all of the <li> tags inside our imagrow
	var litags = document.getElementById("imagerow").getElementsByTagName("li");

	// loop through and add them to iva or ova if they
	// are in or out, respectively, of our desired range
	for (var i = 0; i < litags.length; i++) {
		if ((i < curLeft) || (i > end))
			ova.push(litags[i]);
		else
			iva.push(litags[i]);
	}
	return { in: iva, out: ova };
}

It’s pretty well commented but just to make some things clear: The childNodes and getElementsByTagName collections don’t work exactly like arrays and specifically they don’t support slice(). Therefore, we just make two new arrays and sort through the collection items. The finished product looks like ths:

Go ahead and try some of the events out. The key events only work if the strip has focus, so either click on it, or tab to it first.

If you want to see what it looks like on it’s own, here’s the html, the javascript, and the css


Aug 18 2008

CSS filmstrip

Category: CSSJonathan Fingland @ 3:34 am

Previously, we looked at using CSS opacity to create some interesting effects. In this aricle we’ll be using the CSS opacity technique to create some interesting effects in a filmstrip.

To start with, let’s work from the assumption that all of our thumbnails are the same size and shape. For this example, thumbnails will be 140×100. This is of course, the kind of thing you could change in user preferences for a web app, but let’s start simple. So using a short strip of 3 pictures, we have some XHTML that looks like this.

<img src="/images/mythumb01.jpg"/><img src="/images/mythumb02.jpg"/><img src="/images/mythumb03.jpg"/>

But the first problem we see, of course, is that they are oriented vertically…. which is fine, but let’s try for a horizontal strip, shall we. In the bad old days we’d have done this in a table. I’m talking Netscape 4 days, here *shudder*. Moving quickly on, a good tip for whenever you want strict rows or columns of something is to use an unordered list and some CSS, like this:

<ul class="imageRow">
	<li><img src="/images/mythumb01.jpg"/></li>
	<li><img src="/images/mythumb02.jpg"/></li>
	<li><img src="/images/mythumb03.jpg"/></li>
</ul>
ul.imageRow {
	width: 450px; /*width of all of your images plus the margins/padding you use*/
	height:140px;
}

ul.imageRow li {
	float: left;
	display: block;
	padding: 7px;
	margin: 0px;
}

ul.imageRow li img {
	border: 0px;
	height: 100px; /* just incase the images are a different size */
	width: 140px; /* ditto */
	margin: 0px;
	padding:0px;
}

This will give us something like this:

What if you want to show a lot of images and, say, have the box scroll horizontally? The solution is to add a <div> with a width smaller than the <ul>.

div.filmstrip {
	overflow-x: scroll;
	overflow-y: hidden;
	height: 140px; /* give it enough space for the images and the scrollbar, if present */
}
<div class="filmstrip">
	<ul class="imageRow">
	.
	.
	.
	</ul>
</div>

Which gives us the following:

Now I promised using opacity as a way to highlight pictures, so here it is:

ul.imageRow li {
	float: left;
	display: block;
	padding: 7px;
	margin: 0px;
	opacity: 0.7;
	filter: alpha(opacity=70);
}
ul.imageRow li:hover {
	opacity: 1.0;
	filter: alpha(opacity=100);
}

This gives us:

Here are some other ways to add a highlight to the active picture:

ul.imageRow li {
	float: left;
	display: block;
	padding: 7px;
	margin: 0px;
	background-color: #fff;
}
ul.imageRow li:hover {
	background-color:#bef;
}
ul.imageRow li {
	float: left;
	display: block;
	padding: 7px;
	margin: 0px;
}
ul.imageRow li:hover {
	padding: 4px;  /* IMPORTANT: allows the 3px border to fill the gap so the element
				is the same size as before */
	border: 3px dashed red;
}

Going back to the opacity example, let’s change the background and add an image to give it that filmstrip feel.

div.filmstrip {
	overflow-x: scroll;
	overflow-y: hidden;
	height: 180px; /* give it enough space for the images and the scrollbar, if present (and the
				 filmstrip image) */
	padding: 0px;
	background-color: #000;
}

ul.imageRow {
	width: 1848px; /*width of all of your images plus the margins/padding you use*/
	height: 170px;
	background: url(/images/filmstrip.jpg) 0 0 repeat-x;
	padding: 25px 0px 25px 0px;
	margin: 0px;
}

We could have added the filmstrip background to the div instead of the list, but then the strip doesn’t appear to scroll. Sometimes that’s desirable, here it isn’t. Other things to note are that we’ve added padding to the top and bottom of the unordered list so the thumbnails now fit within the intended area.

Okay, but what about those ugly scroll bars? Well, In the next article we’ll look at some alternatives using Javascript and CSS positioning.

Tags: , , ,


Aug 17 2008

Word macro to insert sequence of numbers

Category: VBAJonathan Fingland @ 9:31 am

I know I promised an article on CSS and Javascript using opacity, but first I’ll share a recent macro i created for a friend of mine.

I used to make macros from time to time, sometimes quite complex ones, for Word and excel, but haven’t for a few years. The following, however, works quite well, and since there was generally a lack of information on how to do just this, I thought I’d share it with you.

First off, here are the three files involved, NumberInsert.frm, NumberInsert.frx and PatentTranslationMacros.bas.

The code for Sequence(), the main macro, is as follows:

Sub Sequence()
	Dim regEx, Match, Matches

	' Get the active word document
	Set objWdDoc = Word.Application.ActiveDocument          

	' Set our range to be the entire document contents
	Set objWdRange = objWdDoc.Content                       

	' To be used for the result string
	Dim Result As String                                    

	' Create a regular expression object.
	Set regEx = CreateObject("VBScript.RegExp")             

	'Show our form
	NumberInsert.Show

	If NumberInsert.Tag Then
		' Get the value of the string we want to find from the form
		regEx.Pattern = NumberInsert.FindStr.Value
		' Set case sensitivity.
		regEx.IgnoreCase = False
		' Set global applicability to false. This was the odd part since
		' no examples on the internet, that I could find, used
		' Globabl = False.
		regEx.Global = False                        

		' A little silly but we have to use something to catch the return value from MoveStart
		Dim temp As Long        

		Dim length As Long, repStart As String, repEnd As String
		' How many digits to show, retrieved from the form and converted to Long
		length = CLng(NumberInsert.NumberLength.Value)
		' the replacement string for the portion BEFORE the inserted number
		repStart = NumberInsert.ReplaceStart
		' the replacement string for the portion AFTER the inserted number
		repEnd = NumberInsert.ReplaceEnd                

		Dim realIndex As Long

		Dim i As Integer        ' For our step counter
	        i = 0
		Do
			' Get the first match (Global = False, remember)
			Set Matches = regEx.Execute(objWdRange) 

			' If there isn't a match, Exit
			If Matches.Count = 0 Then
		                Exit Do
		        End If

			' Get the first match from the MatchCollection.
	        	Set Match = Matches(0)                  

			' Increment our counter. Since we're starting from 1 we increment before we
			' change the value
			i = i + 1                               

			' Convert the characters to full width Japanese characters
			' String(Length, "0") gives us the format pattern for the number
			tStr = StrConv(Format(i, String(length, "0")), vbWide, 1041)    

			' regex replace using the found value and our pattern from the form
			Result = regEx.Replace(Match.Value, repStart & tStr & repEnd)   

			' add the index for the range start and the match index which only considers
			' its position within the range
			realIndex = objWdRange.Start + Match.FirstIndex         

			' Insert our result into the range
			objWdDoc.Range(realIndex, realIndex + Len(Match.Value)).Text = Result   

			' move the start point on our range by the length
			' of our replacement plus the position within the
			' range, plus one
			temp = objWdRange.MoveStart(wdCharacter, Match.FirstIndex + Len(Match.Value) + 1)
        Loop
    End If

End Sub

However, let’s look at some of the individual components in more detail

Set regEx = CreateObject("VBScript.RegExp")

The most portable way to include regular expressions in your macro code is to use the VBScript object – It’s just been around longer than VBA support for regex.

regEx.Global = False

Every example for using regex in VBA or VBScript will show you Global = True, even the MSDN library. Global = False finds the first match only. Otherwise it performs the same. Execute() still returns an array of matches even though there’s only one element.

length = CLng(NumberInsert.NumberLength.Value)
' How many digits to show, retrieved from the form and converted to Long

tStr = StrConv(Format(i, String(length, "0")), vbWide, 1041)
' Convert the characters to full width Japanese characters

length is just the number of digits to show and in the tStr assignment I use String(length, "0") to generate the format string needed, for example if the user wanted to show a number 4 digits long, then the format string produced would be "0000".

tStr also uses the StrConv() function, which takes up to three parameters. The first parameter is the string to convert, the second is the format to convert to, in this case I used vbWide, but other formats include vbLower, vbUpper, vbHiragana, and others.  The best resource for this info is at MSDN. Amazingly, many pages omit over half of the formats available and never included better resources. The third parameter is the Locale ID. In this case, I used 1041, the Japanese locale, because I was converting to full-width and needed a far-east locale to do so.  This table at MSDN has the best info on locale IDs.

' add the index for the range start and the match index which only considers
' its position within the range
realIndex = objWdRange.Start + Match.FirstIndex

As the start point of our range can, and does, change, we need to get the index in the document, not just the index within the range. Simply summing the two values does the job.

temp = objWdRange.MoveStart(wdCharacter, Match.FirstIndex + Len(Match.Value) + 1)
	' move the start point on our range by the length
	' of our replacement plus the position within the
	' range, plus one

Finally, we move the start of the range. MoveStart takes two parameters. The first is the unit to change by. In this case characters (wdCharacter), but you could use wdWord, wdLine, etc. The second parameter is the amount to move by, NOT the position to move to.

Why do a non-global regex match? Why move the start of our range? Well, whenever we insert replacement text that is longer or shorter than the original, the indexes stored in the match array are no longer valid as those character have moved since we executed the regex. Therefore, we need to get one at a time. As to the second question, we want to begin our next step of the search only on the text after our previous match, otherwise the replaced value could also match and we’d end up with an infinite loop continually replacing the first match. And that’s a bad thing, trust me.

The other components of the above are pretty standard and,  if you have a passing familiarity with regex and macros, are hopefully clear from the comments.

The second part of this system is the form. The best way is to import the form file into your word app and have a look, but here’s a screen shot of the form with values filled in.

NumberInsert.frm Example

NumberInsert.frm Example

The code behind it is VERY simple and doesn’t do any validation on the fields. For something more professional you’d definitely want to add some checks on the inputs.

Private Sub CancelBtn_Click()
    Me.Tag = False
    Me.Hide
End Sub

Private Sub NumberLength_Change()
    NumberLengthSpin.Value = NumberLength.Value
    MakeSample
End Sub

Private Sub OKBtn_Click()
    Me.Tag = True
    Me.Hide
End Sub

Private Sub ReplaceEnd_Change()
    MakeSample
End Sub

Private Sub ReplaceStart_Change()
    MakeSample
End Sub

Private Sub MakeSample()
    SampleText.Caption = ReplaceStart.Value & Format("15", String(CLng(NumberLength.Value), "0")) & ReplaceEnd.Value

End Sub

Private Sub NumberLengthSpin_SpinUp()
    NumberLength.Value = NumberLengthSpin.Value
    MakeSample
End Sub

Private Sub NumberLengthSpin_SpinDown()
    NumberLength.Value = NumberLengthSpin.Value
    MakeSample
End Sub

There’s not too much to say about this other than making sure your spin buttons and numerical field update eachother when they’re changed. Me.Tag is used to pass True or False so Sequence() knows if the user canceled (False) or not (True). Finally MakeSample is called from any procedure which can change the value of the sample shown… so almost all of them. Again, adding some checks here   could help. For example, you could check to make sure the user has the right number of ( and ) and that they aren’t nested in any way, and check that the number of stored values called for in the replacement is also equal to the number of (, ) pairs.

Hopefully this helps the next poor soul who goes looking through web pages in the hopes of doing something like this.

Tags: , , ,


Aug 15 2008

Overlapping Opacity

Category: CSSJonathan Fingland @ 1:46 am

In the last article, I showed very briefly styles and structures that included opacity. This time I’m going to focus on that exclusivley along with some examples.

First off, let’s look at how we add opacity, including the Internet Explorer-specific method for cross-browser compatibility.

.opaque {
	background-color: gold
	width: 5.0em;
	opacity: 1.0;
	filter: alpha(opacity=100); /* IE-specific */
}

.translucent {
	background-color: gold
	width: 5.0em;
	opacity: 0.5;
	filter: alpha(opacity=50); /* IE-specific */
}

.transparent {
	background-color: gold;
	width: 5.0em;
	opacity: 0.0;
	filter: alpha(opacity=0); /* IE-specific */
}

<div class="opaque">Some text</div> <div class="transparent">Some text</div> <div class="translucent">Some text</div>

gives us the following. Note that I’ve put the transparent <div> between the opaque and translucent ones so you know where it is.

Some text
Some text
Some text

now, what if you want the text to be opaque against a translucent background? The problem is, the text is a child of the <div> and opacity is inherited.  We can overcome this problem using multiple <div> tags and positioning.

<div id="outer_div" class="outer">
	<div id="background_div" class="translucent">&nbsp;
	</div>
	<div id="content_div" class="cover_text">
		Some Text
	</div>
</div>

And let’s add the two style definitions, cover_text and outer

.outer {
	height: 1.1em;
	position:relative;
}

.cover_text {
	position: absolute;
	height: 1.1em;
	top: 0px;
	left: 0px;
}

Height was included as the only element containing content had position: absolute. You could get around this by adding some text to background_div and setting the color the same as the background-color. Both work just as well.

Now we have:

 
Some Text

Well, now that we have that, what about overlapping opacity like the title says?

Lets look at a few different cases.

.big_red_box {
	height: 10em;
	width: 10em;
	background-color: red;
	position:relative;
}
.medium_yellow_box {
	height: 6em;
	width: 6em;
	top: 2em;
	left: 2em;
	background-color: yellow;
	position:absolute;
}

.small_blue_box {
	height: 2em;
	width: 2em;
	top: 2em;
	left: 2em;
	background-color: blue;
	position:absolute;
}

Making the <div>s nested, largest->smallest gives us

 

Remember that opacity is inherited so our blue box will acquire the same opacity as the yellow box. Adding opacity:0.5; filter: alpha(opacity=50); to .medium_yellow_box, we get:

 

What if we want the blue box to be opaque? Just like the text example before, we don’t make it a child. You’d need to change the position attributes top and left to reflect its position within it's new parent

<div class="big_red_box">
	<div class="medium_yellow_box">&nbsp;
	</div>
	<div class="small_blue_box">
		&nbsp;
	</div>
</div>

Gives us,

 
 

Now while I'm sure that everyone reading this loves boxes in basic colours, we can also do something a little more useful with this. You can use this same technique with images, which is rather more useful than colored boxes. In the next article I'll show how we can use this to make a nice film strip photo preview.


I'll leave you with one last bit

<div style="position:relative;height:3em;width:3em;">
<div style="position:absolute;left:0em;top:0em;height:2em;width:2em;opacity:0.5;filter:alpha(opacity=50);background-color:red;float:left;">&nbsp;</div>
<div style="position:absolute;top:0em; left:1em;height:2em;width:2em;opacity:0.5;filter:alpha(opacity=50);background-color:blue;float:left;">&nbsp;</div>
<div style="position:absolute;top:1em;left:0.5em;height:2em;width:2em;opacity:0.5;filter:alpha(opacity=50);background-color:yellow;margin-bottom:1em;">&nbsp;</div></div>

Gives us:

 
 
 

Tags:


Aug 14 2008

Dynamically Generated and Updated Table

Category: CSS, DOM, JavaScriptJonathan Fingland @ 12:21 am

Although the page itself is pretty bare bones, the script itself is even simpler.  The bandanna guide is another piece I wrote to accompany the game Pirate Quest.

This article attempts to demonstrate how we can use dynamically generated DOM objects to update table data on the fly.  In this case it’s fairly simple as only one value is changing and we’re only changing the cell text. There are some other ideas demonstrated here, such as using opacity and alternating row styles to create a colourful and easy to read table.

The following is the table we’ll be updating. It’s pretty simple – just the column headings.  EDIT: added thead and tbody tags. IE would append rows to the table unless they were in the tbody tag.

<table id="tbl_guide" border="1">
	<thead>
		<tr>
			<td rowspan="2">Level</td>
			<td rowspan="2">Energy</td>
			<td colspan="2">No Bandanna</td>
			<td colspan="2">Bandanna of Vigor</td>
			<td colspan="2">Bandanna of Vitality</td>
			<td colspan="2">Bafunda de la Cabeza</td>
		</tr>
		<tr>
			<td>min</td>
			<td>max</td>
			<td>min</td>
			<td>max</td>
			<td>min</td>
			<td>max</td>
			<td>min</td>
			<td>max</td>
		</tr>
	</thead>
	<tbody id="tbl_body"/>
</table>

You can use whatever you want to start the update process. In this case I used the onchange event on a drop down list

<select id="hideout" onchange="update(Number(this.options[this.selectedIndex].value));">
  <option value="100">homeless (100)</option>
  <option value="105">Wretched Alcove (105)</option>
  <option value="110">Abandoned Outhouse (110)</option>
  <option value="115">Festering Swamp (115)</option>
  <option value="120">Swamp with a View (120)</option>
  <option value="130">Desolate Beach (130)</option>
  <option value="140">Rundown Shanty (140)</option>
  <option value="150">Rusted Roof Shack (150)</option>
  <option value="160">Shanty with a Fence (160)</option>
  <option value="170">Deserted Manor (170)</option>
  <option value="180">Ruined Castle (180)</option>
  <option value="185">Rundown Castle (185)</option>
  <option value="190">Stronghold (190)</option>
  <option value="195">Fortified Stronghold (195)</option>
  <option value="200">Shack on Skull Island (200)</option>
  <option value="220">Cavern on Skull Island (220)</option>
  <option value="230">Stronghold on Skull Island (230)</option>
</select>

Next, on to the script itself. First we define the number of rows and columns, and we create a 2D array for our cells.

//2D array of table cells
var cells = new Array();

var numcols = 10;
var numrows = 600;

I used the body onload event to set up the table rows. EDIT: Because IE doesn’t seem to recognize a class or style change made with setAttribute(), I had to fall back on an alternative

function onLoad() {
	//get our table and our select box
	var table = document.getElementById("tbl_guide");
	var select = document.getElementById("hideout");
	var row; //tr DOM object
	var cell; //td DOM object
	var celltext; //createTextNode DOM object
	var row_arr; //array of cells for each row
	//for the opacity overlay we use three divs. one on the outside, one for the
	//background color and one for the text
	var out_div, in_div1, in_div2; 

	//loop through the number of rows we want
	for (var i = 0; i<numrows; i++) {

		//create this row and set the style for even or odd.
		row = document.createElement("tr");
		row.setAttribute("class",(i%2 == 0)? "even":"odd");
		row.className = (i%2 == 0)? "even":"odd"; //for IE

		//get a new, clean array to work with
		row_arr = new Array();

		//loop through each cell for the number of columns we have
		for (var j=0; j<numcols;j++) {

			//create our table cell and div arrangement for our opacity trick
			cell = document.createElement("td");
			out_div = document.createElement("div");
			in_div1 = document.createElement("div");
			in_div2 = document.createElement("div");

			//set the styles for the div objects
			out_div.setAttribute("class","outer");
			in_div2.setAttribute("class","text");
			in_div1.setAttribute("class","bg");

			//for IE (up to 7) since it appears to lack proper support for setAttribute
			out_div.className = "outer";
			in_div1.className = "bg";
			in_div2.className = "text";

			//depending on what cell we're in we should do different things. mostly this affects the styles
			switch(j) {
				case 0:
					//level
					celltext = document.createTextNode(String(i+1));
					in_div1.setAttribute("style","background-color:yellow;");
					in_div1.style.background = "yellow"; //for IE again
					break;
				case 1:
					//Energy
					celltext = document.createTextNode(String(i+10));
					in_div1.setAttribute("style","background-color:yellow;");
					in_div1.style.background = "yellow";
					break;
				case 2:
					celltext = document.createTextNode("");
					in_div1.setAttribute("style","background-color:red;");
					in_div1.style.background = "red";
					break;
				case 3:
					celltext = document.createTextNode("");
					in_div1.setAttribute("style","background-color:red;");
					in_div1.style.background = "red";
					break;
				case 4:
					celltext = document.createTextNode("");
					in_div1.setAttribute("style","background-color:blue;");
					in_div1.style.background = "blue";
					break;
				case 5:
					celltext = document.createTextNode("");
					in_div1.setAttribute("style","background-color:blue;");
					in_div1.style.background = "blue";
					break;
				case 6:
					celltext = document.createTextNode("");
					in_div1.setAttribute("style","background-color:green;");
					in_div1.style.background = "green";
					break;
				case 7:
					celltext = document.createTextNode("");
					in_div1.setAttribute("style","background-color:green;");
					in_div1.style.background = "green";
					break;
				case 8:
					celltext = document.createTextNode("");
					in_div1.setAttribute("style","background-color:purple;");
					in_div1.style.background = "purple";
					break;
				case 9:
					celltext = document.createTextNode("");
					in_div1.setAttribute("style","background-color:purple;");
					in_div1.style.background = "purple";
					break;
			}

			//add our text to the second inner div
			in_div2.appendChild(celltext);

			//append the two inner divs to the outer div, then append the outer div to the cell
			out_div.appendChild(in_div1);
			out_div.appendChild(in_div2);
			cell.appendChild(out_div);

			//append the cell to the row
			row.appendChild(cell);

			/* finally add this celltext to the row array.
			 * NOTE: you could just as easily append one of the divs or the td tag. In this case
			 * I'm only changing the text. If you wanted to change the colors, for example, you'd
			 * need to push in_div1 onto the array. To change both the color and the text\, you'd
			 * need to push out_div onto the array and later access the children
			 */
			row_arr.push(celltext);
		}

		//add the row to the table, and push the row array onto our 2d cell array
		table.appendChild(row);
		cells.push(row_arr);
	}
	/* then update the values. The operation performed in update() could have been included
	 * in the switch-case above, but then if I changed it, I'd need to change it in two places.
	 * Casts the value from the select box to Number. for *, / , etc. the value is interpreted as
	 * a number anyways, but for +, it assumes it's a string value.... so we have to cast it.
	 */
	update(Number(select.options[select.selectedIndex].value));
}

The onLoad function only assigns values to level and energy.  Hopefully the comments in the code are clear enough to see what I’ve done.

Next up, the update function

function update(value) {
	//loop through each row
	for (var i = 0; i<numrows; i++) {
		//loop through each cell in a row but ignore the first two
		//(values were set up in the onLoad and don't change now)
		for (var j=2; j<numcols;j++) {
			//again switch-case, this time for the different formulas in each cell
			switch(j) {
				case 2:
					//no bandanna min
					cells[i][j].nodeValue = tp((value/150) * ((i+10)/20));
					break;
				case 3:
					//no bandanna max
					cells[i][j].nodeValue = tp((value/75) * ((i+10)/20));
					break;
				case 4:
					//vigor min
					cells[i][j].nodeValue = tp((value/150) * ((i+20)/20));
					break;
				case 5:
					//vigor max
					cells[i][j].nodeValue = tp((value/75) * ((i+20)/20));
					break;
				case 6:
					//vitality min
					cells[i][j].nodeValue = tp(((value + 20)/150) * ((i+10)/20));
					break;
				case 7:
					//vitality max
					cells[i][j].nodeValue = tp(((value + 20)/75) * ((i+10)/20));
					break;
				case 8:
					//bafunda min
					cells[i][j].nodeValue = tp(((value + 25)/150) * ((i+25)/20));
					break;
				case 9:
					//bafunda max
					cells[i][j].nodeValue = tp(((value + 25)/75) * ((i+25)/20));
					break;
			}
		}
	}
}

The update function, as you can see, is very simple. we use .nodeValue to change the text content of our text nodes. As noted in the comments of the onLoad, you could also use the cell itself or one of the containing divs. Your cases would look more like cells[i][j].childNodes[1].childNodes[0].nodeValue if you used out_div in your cells array.

I suppose the last thing to look at is the style section.

table { border-collapse:collapse; border-width:3px; border-style:double; border-color:black; }
td { border-color:black; border-width:1px; }
thead tr td {  padding-left:.5em; }
tbody tr td { padding: 0px 0px 0px 0px; height:1em; width:5em; }
div.outer { position:relative;}
div.bg { opacity: 0.3; height:1em; filter: alpha(opacity=30); padding: 5px 10px 5px 10px; }
div.text { position: absolute; top: 0; bottom: 0; color: black; padding: 5px 10px 5px 10px; }
tbody tr.even { background-color:#d0d0d0; }
tbody tr.odd { background-color:#ffffff; }

It’s pretty short. True, I could have done more to pretty up the page.. and I may yet. but for now, it works well enough and demonstrates the dynamic generation and update technique.

As you can see the code is far from complicated and this would have been far messier in the old pre-DOM days *shudder*.


EDIT: Also, please note that in IE the page is so slow to generate and update that I develop a whole new loathing for Ie every time I open it. In firefox, the generation takes a little time, but not too bad. The updating in Firefox is quite quick. For a small set of rows, IE is fine… 600 rows seemsto kill it though.

Tags: , , , , , ,


Aug 13 2008

Pirate Questing

Category: DOM, Firefox, JavaScript, Pirate Questing, XULJonathan Fingland @ 5:24 am

Pirate Questing is a firefox addon thatI’ve been devloping for a while. It’s available at AMO here

I’ll mostly be using this space to document interesting solutions I come across.

Tags: , ,


« Previous Page