UserScript: Telecom NZ Broadband Stats

I finally got sick of manually calculating my remaining broadband allowance, and wrote a Greasemonkey script that to do it for me.

Update: I’ve updated this script, it now displays remaining bandwidth on the Telecom NZ initial login screen.

It waits until the relevant element has been loaded, then pulls the information it requires from the DOM, converts it into a more readable format, and appends this information into a fixed position div.

Example output (detailed):

Elapsed period:5 days
Remaining period:24 days
Remaining bandwidth:72998 MB
Remaining bandwidth per day:3042 MB

It activates only when there is information to display.

You can view it on it’s UserScripts page – Telecom NZ Usage Stats, install it, or view the source below:

// Telecom NZ Usage Stats
// 
// --------------------------------------------------------------------
//
// This is a Greasemonkey user script.
//
// To install, you need Greasemonkey: https://addons.mozilla.org/en-US/firefox/addon/748
// Then restart Firefox and revisit this script.
// Under Tools, there will be a new menu item to "Install User Script".
// Accept the default configuration and install.
//
// To uninstall, go to Tools/Manage User Scripts,
// select "Telecom NZ Usage Stats", and click Uninstall.
//
// --------------------------------------------------------------------
//
// ==UserScript==
// @name          Telecom NZ Usage Stats
// @namespace     https://www.telecom.co.nz/ebill/requesthandler
// @description   Show Telecom Broadband stats in an easy to read manner
// @include       https://www.telecom.co.nz/ebill/requesthandler*
// ==/UserScript==
 
var telecom_nz_stats = {
 
	BASIC: false,
	DETAILED: false,
	ONE_DAY: 1000 * 60 * 60 * 24,
 
	detailed_url_regex: new RegExp(/^https:\/\/www\.telecom\.co\.nz\/ebill\/requesthandler\/\?js=2&currentAccountLine.*/i),
 
	container: document.createElement('div'),
	label_css: 'style="display:hidden;width:200px;float:left;text-align:right;margin-right:10px;"',
	info_css: 'style="font-weight:bold;"',
 
	interval_id: false,
 
	output: function(content) {
 
		var message = '';
		for (var label in content) {
			message += '<span ' + this.label_css + '>' + label + '</span><span ' + this.info_css + '>' + content[label] + '</span><br/>';
		}
		this.container.innerHTML = message;
		this.container.style.display = 'block';
	},
 
	basic: function() {
		var usage = document.getElementsByClassName('tbdr')[1].getElementsByTagName('table')[0].getElementsByTagName('tr')[2].getElementsByTagName('td')[1].innerHTML.replace(/<\/?(img|nobr|br)[^>]*\/?>|<a\s.*|MB|\s/gi,'').trim().split('of');
		usage = parseFloat(usage[1]) - parseFloat(usage[0]);
		if (!isNaN(usage)) {
			this.output({'Remaining bandwidth:': usage + ' MB'});
		}
	},
 
	detailed: function() {
 
		var total_bandwidth = parseFloat(document.getElementsByClassName('usage')[0].getElementsByTagName('font')[0].children[0].innerHTML.replace(/[^0-9]/g,''));
		var usage_period = document.getElementsByClassName('tbdr')[0].getElementsByTagName('tr')[5].getElementsByTagName('td')[1].innerHTML.split('-');
		var usage_from = new Date(usage_period[0]);
		var usage_to = new Date(usage_period[1]);
		var now = new Date();
 
		var used_bandwidth = parseFloat(document.getElementsByClassName('usage')[1].getElementsByClassName('copy')[0].getElementsByTagName('td')[0].innerHTML.replace(/<\/?(img|nobr|br)[^>]*\/?>|,/gi,'').trim());
 
		var elapsed_days = Math.round((now.getTime() - usage_from.getTime()) / (this.ONE_DAY));
		var total_period = Math.round((usage_to.getTime() - usage_from.getTime()) / (this.ONE_DAY));
		var remaining_days = Math.round((usage_to.getTime() - now.getTime()) / (this.ONE_DAY));
 
		var remaining_bandwidth = (total_bandwidth - used_bandwidth);
 
		this.output({
			'Elapsed period:': elapsed_days + ' days',
			'Remaining period:': remaining_days + ' days',
			'Remaining bandwidth:': Math.round(remaining_bandwidth) + ' MB',
			'Remaining bandwidth per day:': Math.round(remaining_bandwidth / remaining_days) + ' MB'
		});
	},
 
	check: function(self) {
		if (document.getElementsByClassName('tbdr')) {
			window.clearInterval(self.interval_id);
			self.BASIC ? self.basic() : self.detailed();
		}
	},
 
	begin: function() {
		if (this.detailed_url_regex.exec(window.location.href)) this.DETAILED = true;
		else this.BASIC = true;
 
		this.container.style.position = 'fixed';
		this.container.style.top = '10px';
		this.container.style.right = '10px';
		this.container.style.backgroundColor = '#FFF';
		this.container.style.padding = '5px';
		this.container.style.display = 'none';
		document.getElementsByTagName('body')[0].appendChild(this.container);
 
		this.interval_id = window.setInterval(this.check, 100, this); 	
	}
};
 
telecom_nz_stats.begin();
No comments | Trackback

Scriptaculous Draggable reverteffect Example

I had a bit of a hard time finding which arguments are passed to the reverteffect function.

Scriptaculous’ documentation is somewhat opaque:

Effect, default to Effect.Move. Defines the effect to use when the draggable reverts back to its starting position.

Hmm, no mention of any arguments passed to the reverteffect function…

Here is an example of instantiating a new Draggable with a custom reverteffect:

new Draggable( element, {
    revert: true,
    reverteffect: function( element, top_offset, left_offset ){
        new Effect.Fade( element, {
            afterFinish: function(){
                $( element ).setStyle({
                    top: '0px',
                    left: '0px'
                });
                new Effect.Appear( element );
            }
        });
    }
});

Note that the arguments top_offset and left_offset are the offsets of the draggable as it was after the drag was completed. Also note that in my custom reverteffect function I’ve reset the draggable’s top and left offsets to 0, effectively shoving it back to it’s original position.

No comments | Trackback

IE8 & Assigning Classnames With new Element(‘…’)

When dynamically creating elements using Prototype’s new Element(‘…’) method, I ran into this irritating gotcha with IE8.

While the following looks great and works well in Firefox:

new Element('span', { 'class': 'someClassName' });

In IE8, not so much.

What one expects, and gets in Firefox:

<span class="someClassName"></span>

What one gets in IE8:

<span></span>

Hmmm, where did that classname go? Yeah, I don’t know either.

To avoid this, one must perform the following incantation:

new Element('span').addClassName('someClassName');

This will give the same result in Firefox and IE8. Tadaa!

No comments | Trackback

Scriptaculous Effect.Morph IE6

Ran into a weird bug the other day in IE6. Some elements, h1′s, weren’t animating with their parent when the parent was being Effect.Morphed. Every other element was moving, but the h1′s just sat there, looking really stupid. I think the use of Twin Helix’s IE PNG Fix was a contributing factor here, since writing this article I’ve noticed ( and fixed, using the solution below ) this issue on other position:relative, PNG fixed elements.

The h1′s were the parents referred to in the post Float:Right Nuke for IE6, and thus had position:relative set. Removing this caused the h1′s to animate nicely, but unfortunately I couldn’t do that because removing the position:relative had the side-effect of causing the content that I wanted to simulate float:right with to shoot up to the top-right of the screen. D’oh.

To solve it, and have everything animate acceptable in IE6 I had to do this:

/* the first morph: */
new Effect.Morph('h1_parent', { style: 'padding-top:50px', delay: 0.5 });
 
/* the fix: */
$$('.h1_className').each( function(element){ 
	new Effect.Morph(element, { 
		style: 'top:1px', duration: 0.5 
	});
});
 
/* the second morph, taking place after some user action: */
new Effect.Morph('h1_parent', { style: 'padding-top:10px', delay: 0.5 });
 
/* the fix: */
$$('.h1_className').each( function(element){ 
	new Effect.Morph(element, { 
		style: 'top:0px', duration: 0.5 
	});
});

Yeah, looks stupid I know. Works though. A bunch of h1′s moving 1px isn’t noticeable, but h1′s that didn’t animate sure would have been!

One more time, combined into a more usable function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
 * Wiggles elements with the given classes to make sure they animate 
 * with their parent, their parent's parents, and their parent's parent's 
 * parents ( ad infinitum ) in IE6, when the elements of the given
 *  classes have the Twin Helix PNG fix + position:relative applied
 *
 * @param classes an array of css class names, prefixed with a '.'
 */
wiggle = function( classes ){
classes.each( function( className ){
     $$( className ).each( function( ele ){
          new Effect.Morph( ele, {
               style: 'top:1px', 
               duration: 0.25,
               afterFinish: function(){
                    new Effect.Morph( ele, {
                         style: 'top:0px',
                         duration: 0.25
                    });
               }
     });
}
 
/* the first morph: */
new Effect.Morph('h1_parent', { style: 'padding-top:50px', delay: 0.5 });
wiggle( [ '.classOne', '.classTwo' ] );
 
/* the second morph, taking place after some user action: */
new Effect.Morph('h1_parent', { style: 'padding-top:10px', delay: 0.5 });
wiggle( [ '.classOne', '.classTwo' ] );
No comments | Trackback

Check if it’s an Object!

OK Javascripters, check it out: the IE family don’t like you. Or they love you. I’m not sure. Maybe they just want you to be better? To produce perfect absolutely syntax-error free code. Code that caters for even the most careful pedantic Javascript interpreters.

Example:

In the WYSIWYG text editor we use, when one clicks a UI button that is meant to cause say, an “Embed Flash” dialog to popup, there is a section of Javascript devoted to building said dialog and binding functions, style structure to it. My casual inspection of the code leads me to believe that it stores the dialog as an array of objects and functions. Now, when the dialog is supposed to do it’s popping up, this array is iterated over. Certain things are meant to happen to certain objects, objects with certain properties.

Notice how I said objects there. Objects. Not the functions.

So then, we should test each element to make sure it’s a function, right? Well yeah, we should. But the code wasn’t. Firefox didn’t complain, why would it!? If it’s a function, it won’t have attribute x – it’s a function, dummy! Firefox knows this. Safari does too. The IE family knows ( I guess ), but it wants you to be better ( loves ) or wants you to go insane ( hates ).

See when one of the IE’s hits a function and is asked to test whether it has a property, like this:

if( item.type == 'car' || item.type == 'boat' ){ /* do stuff! */ }

It refuses to cooperate. One is simply told: “‘type’ is null or not an object“.

Helpful!

What it really means, in this case is that IE is too dumb confused and needs you to hold it’s hand.

By hold its hand I mean do something like this:

if( ( typeof( item ) == 'object' ) ){
    if( item.type == 'car' || item.type == 'boat' ){ /* do stuff! */ }
}

First, test to make sure it’s an object! If you’re iterating a structure that contains mixed types, and you want to do something to a specific type, test for the type before diving into type-specific conditionals! It will save your sanity points when it comes time for you, or others when performing browser-compatibility testing.

And if you work on a publicly available project, you’ll be saving other people’s sanity as well ;)

No comments | Trackback